diff --git a/src/backends/qalculate/qalculatehighlighter.cpp b/src/backends/qalculate/qalculatehighlighter.cpp index 6f8278d4..c7aa68d1 100644 --- a/src/backends/qalculate/qalculatehighlighter.cpp +++ b/src/backends/qalculate/qalculatehighlighter.cpp @@ -1,131 +1,132 @@ /************************************************************************************ * Copyright (C) 2009 by Milian Wolff * * * * This program 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 2 * * of the License, or (at your option) any later version. * * * * This program 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 this program; if not, write to the Free Software * * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA * *************************************************************************************/ #include "qalculatehighlighter.h" #include #include #include #include #include #include #include #include +#include QalculateHighlighter::QalculateHighlighter(QObject* parent) : Cantor::DefaultHighlighter(parent) { } void QalculateHighlighter::highlightBlock(const QString& text) { if ( text.isEmpty() || text.trimmed().isEmpty() || text.startsWith(QLatin1String(">>> ")) // filter error messages, they get highlighted via html || text.startsWith(i18n("ERROR") + QLatin1Char(':')) || text.startsWith(i18n("WARNING") + QLatin1Char(':')) ) { return; } int pos = 0; int count; ///TODO: Can't we use CALCULATOR->parse() or similar? /// Question is how to get the connection between /// MathStructur and position+length in @p text - const QStringList& words = text.split(QRegExp(QLatin1String("\\b")), QString::SkipEmptyParts); + const QStringList words = text.split(QRegularExpression(QStringLiteral("\\b")), QString::SkipEmptyParts); qDebug() << "highlight block:" << text; CALCULATOR->beginTemporaryStopMessages(); const QString decimalSymbol = QLocale().decimalPoint(); for ( int i = 0; i < words.size(); ++i, pos += count ) { count = words[i].size(); if ( words[i].trimmed().isEmpty() ) { continue; } qDebug() << "highlight word:" << words[i]; QTextCharFormat format = errorFormat(); if ( i < words.size() - 1 && words[i+1].trimmed() == QLatin1String("(") && CALCULATOR->getFunction(words[i].toUtf8().constData()) ) { // should be a function qDebug() << "function"; format = functionFormat(); } else if ( isOperatorAndWhitespace(words[i]) ) { // stuff like ") * (" is an invalid expression, but actually OK // check if last number is actually a float bool isFloat = false; if ( words[i].trimmed() == decimalSymbol ) { if ( i > 0 ) { // lookbehind QString lastWord = words[i-1].trimmed(); if ( !lastWord.isEmpty() && lastWord.at(lastWord.size()-1).isNumber() ) { qDebug() << "actually float"; isFloat = true; } } if ( !isFloat && i < words.size() - 1 ) { // lookahead QString nextWord = words[i+1].trimmed(); if ( !nextWord.isEmpty() && nextWord.at(0).isNumber() ) { qDebug() << "float coming"; isFloat = true; } } } if ( !isFloat ) { qDebug() << "operator / whitespace"; format = operatorFormat(); } else { format = numberFormat(); } } else { MathStructure expr = CALCULATOR->parse(words[i].toLatin1().constData()); if ( expr.isNumber() || expr.isNumber_exp() ) { qDebug() << "number"; format = numberFormat(); } else if ( expr.isVariable() ) { qDebug() << "variable"; format = variableFormat(); } else if ( expr.isUndefined() ) { qDebug() << "undefined"; } else if ( expr.isUnit() || expr.isUnit_exp() ) { qDebug() << "unit"; format = keywordFormat(); } } setFormat(pos, count, format); } CALCULATOR->endTemporaryStopMessages(); } bool QalculateHighlighter::isOperatorAndWhitespace(const QString& word) const { foreach ( const QChar& c, word ) { if ( c.isLetterOrNumber() ) { return false; } } return true; } diff --git a/src/backends/qalculate/qalculatesession.cpp b/src/backends/qalculate/qalculatesession.cpp index 4d599275..f5590310 100644 --- a/src/backends/qalculate/qalculatesession.cpp +++ b/src/backends/qalculate/qalculatesession.cpp @@ -1,430 +1,424 @@ /************************************************************************************ * Copyright (C) 2009 by Milian Wolff * * Copyright (C) 2011 by Matteo Agostinelli * * * * This program 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 2 * * of the License, or (at your option) any later version. * * * * This program 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 this program; if not, write to the Free Software * * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA * *************************************************************************************/ #include "settings.h" #include "qalculatesession.h" #include "qalculatecompletionobject.h" #include "qalculatehighlighter.h" #include "defaultvariablemodel.h" #include -#include +#include #include #include #include #include #include #include #include "qalculatesyntaxhelpobject.h" QalculateSession::QalculateSession( Cantor::Backend* backend) : Session(backend), m_variableModel(new Cantor::DefaultVariableModel(this)), m_process(nullptr), m_currentExpression(nullptr), m_isSaveCommand(false) { /* qalc does all of this by default but we still need the CALCULATOR instance for plotting graphs */ if ( !CALCULATOR ) { new Calculator(); CALCULATOR->loadGlobalDefinitions(); CALCULATOR->loadLocalDefinitions(); CALCULATOR->loadExchangeRates(); } } QalculateSession::~QalculateSession() { CALCULATOR->abort(); if(m_process) { m_process->kill(); m_process->deleteLater(); m_process = nullptr; } } void QalculateSession::login() { if (m_process) return; emit loginStarted(); qDebug() << "login started"; /* we will , most probably, use autoscripts for setting the mode , evaluate options, print options etc */ // if(!QalculateSettings::autorunScripts().isEmpty()){ // QString autorunScripts = QalculateSettings::self()->autorunScripts().join(QLatin1String("\n")); // // evaluateExpression(autorunScripts, QalculateExpression::DeleteOnFinish); // } /* set up the process here. The program path , arguments(if any),channel modes , and connections should all be set up here. once the setup is complete, start the process and inform the worksheet that we are ready */ m_process = new QProcess(this); m_process->setProgram(QStandardPaths::findExecutable(QLatin1String("qalc"))); m_process->setProcessChannelMode(QProcess::SeparateChannels); connect(m_process, SIGNAL(readyReadStandardOutput()), this, SLOT(readOutput())); connect(m_process, SIGNAL(readyReadStandardError()), this, SLOT(readError())); connect(m_process, SIGNAL(started()), this, SLOT(processStarted())); m_process->start(); changeStatus(Session::Done); emit loginDone(); } void QalculateSession::readOutput() { while(m_process->bytesAvailable()) { m_output.append(QString::fromLocal8Bit(m_process->readLine())); qDebug() << m_output << endl; } if(m_currentExpression && !m_output.isEmpty() && m_output.trimmed().endsWith(QLatin1String(">"))) { // check if the commandQueue is empty or not . if it's not empty run the "runCommandQueue" function. // store the output in finalOutput and clear m_output if(m_currentCommand.trimmed().isEmpty()) m_output.clear(); if(!m_output.toLower().contains(QLatin1String("error")) && m_isSaveCommand) { storeVariables(m_currentCommand, m_output); m_isSaveCommand = false; } m_output = m_output.trimmed(); m_output.remove(m_currentCommand); if (!m_output.isEmpty()) m_finalOutput.append(m_output); // we tried to perform a save operation but failed(see parseSaveCommand()).In such a case // m_output will be empty but m_saveError will contain the error message. if(!m_saveError.isEmpty()) { m_finalOutput.append(m_saveError); m_saveError.clear(); } m_finalOutput.append(QLatin1String("\n")); m_output.clear(); if (!m_commandQueue.isEmpty()) runCommandQueue(); else { qDebug () << "parsing output: " << m_finalOutput << endl; m_currentExpression->parseOutput(m_finalOutput); m_finalOutput.clear(); } } } void QalculateSession::storeVariables(QString& currentCmd, QString output) { // internally we pass save(value,variable) command to qlac to save the variables. see parseSaveCommand() // TODO: if the user if trying to override a default variable(constants etc) or an existing variable, ask the user if he/she wants to override it or not. qDebug() << "save command " << currentCmd << endl; /** if we have reached here, we expect our variable model to be updated with new variables. In case the variable model is not updated, it most probably because we were not able to successfully parse the current command and output to extract variable and value This is probably not the best way to get the variable and value. But since qalc does not provide a way to get the list of variables, we will have to stick to parsing **/ - QString value; - QString var; - QRegExp regex; + QRegularExpression regex; // find the value - regex.setPattern(QLatin1String("[\\s\\w\\W]+=\\s*([\\w\\W]+)")); - if(regex.exactMatch(output)) { - int pos = regex.indexIn(output); - if (pos > -1) { - value = regex.cap(1); - value = value.trimmed(); - value.replace(QLatin1String("\n"), QLatin1String("")); - value.remove(QLatin1String(">")); - } + regex.setPattern(QStringLiteral("^[\\s\\w\\W]+=\\s*([\\w\\W]+)$")); + QRegularExpressionMatch match = regex.match(output); + QString value; + if(match.hasMatch()) { + value = match.captured(1).trimmed(); + value.replace(QLatin1String("\n"), QLatin1String("")); + value.remove(QLatin1String(">")); } //find the varaiable. // ex1: currentCmd = save(10, var_1,category, title): var_1 = variable // ex2: currentCmd = save(planet(jupter,mass), jupiter_mass, category, title): jupiter_mass = variable - // Not the best regex. Cab be improved - regex.setPattern(QLatin1String("\\s*save\\s*\\(\\s*[\\s\\w]+\\s*,([\\s\\w]+),*[\\w\\W]*\\)\\s*;*$|\\s*save\\s*\\(\\s*[\\s\\w\\W]+\\)\\s*,([\\s\\w]+),*[\\w\\W]*\\)\\s*;*$")); - if(regex.exactMatch(currentCmd)) { - int pos = regex.indexIn(currentCmd); - if (pos > -1) { - if(!regex.cap(1).trimmed().isEmpty()) - var = regex.cap(1).trimmed(); - else - var = regex.cap(2).trimmed(); - - var = var.trimmed(); - var.replace(QLatin1String("\n"), QLatin1String("")); - var.remove(QLatin1String(">")); - } + + // regex.setPattern(QLatin1String("\\s*save\\s*\\(\\s*[\\s\\w]+\\s*,([\\s\\w]+),*[\\w\\W]*\\)\\s*;*$|\\s*save\\s*\\(\\s*[\\s\\w\\W]+\\)\\s*,([\\s\\w]+),*[\\w\\W]*\\)\\s*;*$")); + + regex.setPattern(QStringLiteral("^\\s*save\\s*\\(" + "(?:.+?(?:\\(.+?,.+?\\))|(?:[^,()]+?))," + "(.+?)," + "(?:.+?)," + "(?:.+?)\\)\\s*;?$")); + QString var; + match = regex.match(currentCmd); + if (match.hasMatch()) { + var = match.captured(1).trimmed(); + var.replace(QLatin1String("\n"), QLatin1String("")); + var.remove(QLatin1String(">")); } if(!value.isEmpty() && !var.isEmpty()) variables.insert(var, value); } void QalculateSession::readError() { QString error = QLatin1String(m_process->readAllStandardError()); if(m_currentExpression) { m_currentExpression->parseError(error); } } void QalculateSession::processStarted() { qDebug() << "process started " << m_process->program() << m_process->processId() << endl; } void QalculateSession::logout() { qDebug () << "logging out " << endl; if (!m_process) return; if(status() == Cantor::Session::Running) interrupt(); m_process->write("quit\n"); if(!m_process->waitForFinished(1000)) m_process->kill(); m_process->deleteLater(); m_process = nullptr; Session::logout(); } void QalculateSession::interrupt() { qDebug () << "interrupting .... " << endl; if(m_currentExpression) m_currentExpression->interrupt(); m_commandQueue.clear(); m_expressionQueue.clear(); m_output.clear(); m_finalOutput.clear(); m_currentCommand.clear(); m_currentExpression = nullptr; } void QalculateSession::runExpression() { const QString& command = m_currentExpression->command(); foreach(const QString& cmd, command.split(QLatin1Char('\n'))) { m_commandQueue.enqueue(cmd); } runCommandQueue(); } void QalculateSession::runCommandQueue() { if (!m_commandQueue.isEmpty()) { m_currentCommand = m_commandQueue.dequeue(); // parse the current command if it's a save/load/store command if( m_currentCommand.toLower().trimmed().startsWith(QLatin1String("save")) || m_currentCommand.toLower().trimmed().startsWith(QLatin1String("store")) || m_currentCommand.trimmed().startsWith(QLatin1String("saveVariables"))) { m_currentCommand = parseSaveCommand(m_currentCommand); } m_currentCommand = m_currentCommand.trimmed(); m_currentCommand += QLatin1String("\n"); m_process->write(m_currentCommand.toLocal8Bit()); } } QString QalculateSession::parseSaveCommand(QString& currentCmd) { /* make sure the command is: * formatted correctly. e.g if the command is save(value,variable), we have to make sure that there is no space between save and '(', otherwise qalc waits for user input which is not supported by us as of now * supported save commands: save(value,variable,[category],[title]), save definitions, save mode, save var, store var, saveVariables filename */ - QRegExp regex; - regex.setCaseSensitivity(Qt::CaseInsensitive); + QRegularExpression regex; + regex.setPatternOptions(QRegularExpression::CaseInsensitiveOption); - regex.setPattern(QLatin1String("\\s*save\\s*definitions\\s*")); - if(regex.exactMatch(currentCmd)) { + regex.setPattern(QStringLiteral("^\\s*save\\s*definitions\\s*$")); + if(regex.match(currentCmd).hasMatch()) { // save the variables in ~/.cantor/backends/qalculate/definitions currentCmd.clear(); return currentCmd; } - regex.setPattern(QLatin1String("\\s*save\\s*mode\\s*")); - if(regex.exactMatch(currentCmd)) { + regex.setPattern(QStringLiteral("^\\s*save\\s*mode\\s*$")); + if(regex.match(currentCmd).hasMatch()) { // save the mode in ~/.cantor/backends/qalculate/cantor_qalc.cfg currentCmd.clear(); return currentCmd; } - regex.setPattern(QLatin1String("\\s*saveVariables\\s*[\\w\\W]+")); - if(regex.exactMatch(currentCmd)) { + regex.setPattern(QStringLiteral("^\\s*saveVariables\\s*[\\w\\W]+$")); + if(regex.match(currentCmd).hasMatch()) { // save the variables in a file currentCmd.clear(); return currentCmd; } - regex.setPattern(QLatin1String("\\s*store\\s*([a-zA-Z_]+[\\w]*)|\\s*save\\s*([a-zA-Z_]+[\\w]*)")); - if(regex.exactMatch(currentCmd)) { + regex.setPattern(QStringLiteral("^\\s*store\\s*([a-zA-Z_]+[\\w]*)|\\s*save\\s*([a-zA-Z_]+[\\w]*)$")); + QRegularExpressionMatch match = regex.match(currentCmd); + if(match.hasMatch()) { m_isSaveCommand = true; - int pos = regex.indexIn(currentCmd); - if(pos > -1) { - if(!regex.cap(1).trimmed().isEmpty()) - currentCmd = QStringLiteral("save(%1, %2)").arg(QStringLiteral("ans")).arg(regex.cap(1).trimmed()); - else - currentCmd = QStringLiteral("save(%1, %2)").arg(QStringLiteral("ans")).arg(regex.cap(2).trimmed()); - - return currentCmd; - } + + QString str = match.captured(1).trimmed(); + if (str.isEmpty()) + str = match.captured(2).trimmed(); + + currentCmd = QStringLiteral("save(%1, %2)").arg(QStringLiteral("ans"), str); + + return currentCmd; } - regex.setPattern(QLatin1String("\\s*save\\s*(\\([\\w\\W]+\\))\\s*;*$")); - if(regex.exactMatch(currentCmd)) { + regex.setPattern(QStringLiteral("^\\s*save\\s*(\\([\\w\\W]+\\))\\s*;*$")); + match = regex.match(currentCmd); + if(match.hasMatch()) { m_isSaveCommand = true; - int pos = regex.indexIn(currentCmd); - if (pos > -1) { - currentCmd = QStringLiteral("save%1").arg(regex.cap(1).trimmed()); - return currentCmd; - } + currentCmd = QStringLiteral("save%1").arg(match.captured(1).trimmed()); + return currentCmd; } /* If we have not returned by this point, it's because: * we did not parse the save command properly. This might be due to malformed regular expressions. * or the commnad given by the user is malformed. More likely to happen In both these cases we will simply return an empty string because we don't want qalc to run malformed queries, else it would wait for user input and hence Qprocess would never return a complete output and the expression will remain in 'calculating' state */ m_saveError = currentCmd + QLatin1String("\nError: Could not save.\n"); return QLatin1String(""); } void QalculateSession::currentExpressionStatusChanged(Cantor::Expression::Status status) { // depending on the status of the expression change the status of the session; switch (status) { case Cantor::Expression::Computing: break; case Cantor::Expression::Interrupted: changeStatus(Cantor::Session::Done); break; case Cantor::Expression::Queued: break; case Cantor::Expression::Done: case Cantor::Expression::Error: qDebug() << " ****** STATUS " << status; changeStatus(Cantor::Session::Done); if(m_expressionQueue.size() > 0) m_expressionQueue.dequeue(); if(!m_expressionQueue.isEmpty()) runExpressionQueue(); } } Cantor::Expression* QalculateSession::evaluateExpression(const QString& cmd, Cantor::Expression::FinishingBehavior behave, bool internal) { qDebug() << " ** evaluating expression: " << cmd << endl; qDebug() << " size of expression queue: " << m_expressionQueue.size() << endl; changeStatus(Cantor::Session::Running); QalculateExpression* expr = new QalculateExpression(this, internal); expr->setFinishingBehavior(behave); expr->setCommand(cmd); m_expressionQueue.enqueue(expr); runExpressionQueue(); return expr; } void QalculateSession::runExpressionQueue() { if(!m_expressionQueue.isEmpty()) { if(!m_currentExpression) m_currentExpression = m_expressionQueue.head(); else { /* there was some expression that was being executed by cantor. We run the new expression only if the current expression's status is 'Done' or 'Error', if not , we simply return */ Cantor::Expression::Status expr_status = m_currentExpression->status(); if(expr_status != Cantor::Expression::Done && expr_status != Cantor::Expression::Error) return; } m_currentExpression = m_expressionQueue.head(); connect(m_currentExpression, SIGNAL(statusChanged(Cantor::Expression::Status)), this, SLOT(currentExpressionStatusChanged(Cantor::Expression::Status))); // start processing the expression m_currentExpression->evaluate(); } } Cantor::CompletionObject* QalculateSession::completionFor(const QString& command, int index) { return new QalculateCompletionObject(command, index, this); } Cantor::SyntaxHelpObject* QalculateSession::syntaxHelpFor(const QString& cmd) { return new QalculateSyntaxHelpObject(cmd, this); } QSyntaxHighlighter* QalculateSession::syntaxHighlighter(QObject* parent) { return new QalculateHighlighter(parent); } Cantor::DefaultVariableModel* QalculateSession::variableModel() const { return m_variableModel; }