diff options
| author | Christian Kandeler <christian.kandeler@digia.com> | 2013-11-14 11:11:22 +0100 |
|---|---|---|
| committer | Christian Kandeler <christian.kandeler@digia.com> | 2013-11-14 13:11:22 +0100 |
| commit | 8c0f155a98f0cee77422577a06329d8b7f32ce1b (patch) | |
| tree | ccedd829b59c1f1f7578854934742eff53688c1f | |
| parent | 9e89d3a109a3d82c80f2d51552849f980bc32428 (diff) | |
| download | qbs-8c0f155a98f0cee77422577a06329d8b7f32ce1b.tar.gz | |
Import code for rewriting QML files.
This comes from Qt Creator and is needed for API functionality such
as adding files to a project.
Change-Id: Ia5ecef1d5a5d937cf3fbf27448abbce051e977de
Reviewed-by: Joerg Bornemann <joerg.bornemann@digia.com>
| -rw-r--r-- | src/lib/api/api.pri | 4 | ||||
| -rw-r--r-- | src/lib/api/changeset.cpp | 387 | ||||
| -rw-r--r-- | src/lib/api/changeset.h | 130 | ||||
| -rw-r--r-- | src/lib/api/qmljsrewriter.cpp | 718 | ||||
| -rw-r--r-- | src/lib/api/qmljsrewriter.h | 120 | ||||
| -rw-r--r-- | src/lib/lib.pro | 2 | ||||
| -rw-r--r-- | src/lib/lib.qbs | 6 |
7 files changed, 1365 insertions, 2 deletions
diff --git a/src/lib/api/api.pri b/src/lib/api/api.pri index 61632356b..329675b37 100644 --- a/src/lib/api/api.pri +++ b/src/lib/api/api.pri @@ -1,4 +1,5 @@ HEADERS += \ + $$PWD/changeset.h \ $$PWD/internaljobs.h \ $$PWD/projectdata.h \ $$PWD/runenvironment.h \ @@ -6,13 +7,16 @@ HEADERS += \ $$PWD/project.h \ $$PWD/propertymap_p.h \ $$PWD/projectdata_p.h + $$PWD/qmljsrewriter.h SOURCES += \ + $$PWD/changeset.cpp \ $$PWD/internaljobs.cpp \ $$PWD/runenvironment.cpp \ $$PWD/projectdata.cpp \ $$PWD/jobs.cpp \ $$PWD/project.cpp + $$PWD/qmljsrewriter.cpp !qbs_no_dev_install { api_headers.files = $$PWD/projectdata.h $$PWD/runenvironment.h $$PWD/jobs.h $$PWD/project.h diff --git a/src/lib/api/changeset.cpp b/src/lib/api/changeset.cpp new file mode 100644 index 000000000..6a7fbcc6b --- /dev/null +++ b/src/lib/api/changeset.cpp @@ -0,0 +1,387 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt Build Suite. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ + +#include "changeset.h" + +#include <QTextCursor> + +namespace QbsQmlJS { + +ChangeSet::ChangeSet() + : m_string(0), m_cursor(0), m_error(false) +{ +} + +ChangeSet::ChangeSet(const QList<EditOp> &operations) + : m_string(0), m_cursor(0), m_operationList(operations), m_error(false) +{ +} + +static bool overlaps(int posA, int lengthA, int posB, int lengthB) { + if (lengthB > 0) { + return + // right edge of B contained in A + (posA < posB + lengthB && posA + lengthA >= posB + lengthB) + // left edge of B contained in A + || (posA <= posB && posA + lengthA > posB) + // A contained in B + || (posB < posA && posB + lengthB > posA + lengthA); + } else { + return (posB > posA && posB < posA + lengthA); + } +} + +bool ChangeSet::hasOverlap(int pos, int length) +{ + QListIterator<EditOp> i(m_operationList); + while (i.hasNext()) { + const EditOp &cmd = i.next(); + + switch (cmd.type) { + case EditOp::Replace: + if (overlaps(pos, length, cmd.pos1, cmd.length1)) + return true; + break; + + case EditOp::Move: + if (overlaps(pos, length, cmd.pos1, cmd.length1)) + return true; + if (cmd.pos2 > pos && cmd.pos2 < pos + length) + return true; + break; + + case EditOp::Insert: + if (cmd.pos1 > pos && cmd.pos1 < pos + length) + return true; + break; + + case EditOp::Remove: + if (overlaps(pos, length, cmd.pos1, cmd.length1)) + return true; + break; + + case EditOp::Flip: + if (overlaps(pos, length, cmd.pos1, cmd.length1)) + return true; + if (overlaps(pos, length, cmd.pos2, cmd.length2)) + return true; + break; + + case EditOp::Copy: + if (overlaps(pos, length, cmd.pos1, cmd.length1)) + return true; + if (cmd.pos2 > pos && cmd.pos2 < pos + length) + return true; + break; + + case EditOp::Unset: + break; + } + } + + return false; +} + +bool ChangeSet::isEmpty() const +{ + return m_operationList.isEmpty(); +} + +QList<ChangeSet::EditOp> ChangeSet::operationList() const +{ + return m_operationList; +} + +void ChangeSet::clear() +{ + m_string = 0; + m_cursor = 0; + m_operationList.clear(); + m_error = false; +} + +bool ChangeSet::replace_helper(int pos, int length, const QString &replacement) +{ + if (hasOverlap(pos, length)) + m_error = true; + + EditOp cmd(EditOp::Replace); + cmd.pos1 = pos; + cmd.length1 = length; + cmd.text = replacement; + m_operationList += cmd; + + return !m_error; +} + +bool ChangeSet::move_helper(int pos, int length, int to) +{ + if (hasOverlap(pos, length) + || hasOverlap(to, 0) + || overlaps(pos, length, to, 0)) + m_error = true; + + EditOp cmd(EditOp::Move); + cmd.pos1 = pos; + cmd.length1 = length; + cmd.pos2 = to; + m_operationList += cmd; + + return !m_error; +} + +bool ChangeSet::insert(int pos, const QString &text) +{ + Q_ASSERT(pos >= 0); + + if (hasOverlap(pos, 0)) + m_error = true; + + EditOp cmd(EditOp::Insert); + cmd.pos1 = pos; + cmd.text = text; + m_operationList += cmd; + + return !m_error; +} + +bool ChangeSet::replace(const Range &range, const QString &replacement) +{ return replace(range.start, range.end, replacement); } + +bool ChangeSet::remove(const Range &range) +{ return remove(range.start, range.end); } + +bool ChangeSet::move(const Range &range, int to) +{ return move(range.start, range.end, to); } + +bool ChangeSet::flip(const Range &range1, const Range &range2) +{ return flip(range1.start, range1.end, range2.start, range2.end); } + +bool ChangeSet::copy(const Range &range, int to) +{ return copy(range.start, range.end, to); } + +bool ChangeSet::replace(int start, int end, const QString &replacement) +{ return replace_helper(start, end - start, replacement); } + +bool ChangeSet::remove(int start, int end) +{ return remove_helper(start, end - start); } + +bool ChangeSet::move(int start, int end, int to) +{ return move_helper(start, end - start, to); } + +bool ChangeSet::flip(int start1, int end1, int start2, int end2) +{ return flip_helper(start1, end1 - start1, start2, end2 - start2); } + +bool ChangeSet::copy(int start, int end, int to) +{ return copy_helper(start, end - start, to); } + +bool ChangeSet::remove_helper(int pos, int length) +{ + if (hasOverlap(pos, length)) + m_error = true; + + EditOp cmd(EditOp::Remove); + cmd.pos1 = pos; + cmd.length1 = length; + m_operationList += cmd; + + return !m_error; +} + +bool ChangeSet::flip_helper(int pos1, int length1, int pos2, int length2) +{ + if (hasOverlap(pos1, length1) + || hasOverlap(pos2, length2) + || overlaps(pos1, length1, pos2, length2)) + m_error = true; + + EditOp cmd(EditOp::Flip); + cmd.pos1 = pos1; + cmd.length1 = length1; + cmd.pos2 = pos2; + cmd.length2 = length2; + m_operationList += cmd; + + return !m_error; +} + +bool ChangeSet::copy_helper(int pos, int length, int to) +{ + if (hasOverlap(pos, length) + || hasOverlap(to, 0) + || overlaps(pos, length, to, 0)) + m_error = true; + + EditOp cmd(EditOp::Copy); + cmd.pos1 = pos; + cmd.length1 = length; + cmd.pos2 = to; + m_operationList += cmd; + + return !m_error; +} + +void ChangeSet::doReplace(const EditOp &replace_helper, QList<EditOp> *replaceList) +{ + Q_ASSERT(replace_helper.type == EditOp::Replace); + + { + QMutableListIterator<EditOp> i(*replaceList); + while (i.hasNext()) { + EditOp &c = i.next(); + if (replace_helper.pos1 <= c.pos1) + c.pos1 += replace_helper.text.size(); + if (replace_helper.pos1 < c.pos1) + c.pos1 -= replace_helper.length1; + } + } + + if (m_string) { + m_string->replace(replace_helper.pos1, replace_helper.length1, replace_helper.text); + } else if (m_cursor) { + m_cursor->setPosition(replace_helper.pos1); + m_cursor->setPosition(replace_helper.pos1 + replace_helper.length1, QTextCursor::KeepAnchor); + m_cursor->insertText(replace_helper.text); + } +} + +void ChangeSet::convertToReplace(const EditOp &op, QList<EditOp> *replaceList) +{ + EditOp replace1(EditOp::Replace); + EditOp replace2(EditOp::Replace); + + switch (op.type) { + case EditOp::Replace: + replaceList->append(op); + break; + + case EditOp::Move: + replace1.pos1 = op.pos1; + replace1.length1 = op.length1; + replaceList->append(replace1); + + replace2.pos1 = op.pos2; + replace2.text = textAt(op.pos1, op.length1); + replaceList->append(replace2); + break; + + case EditOp::Insert: + replace1.pos1 = op.pos1; + replace1.text = op.text; + replaceList->append(replace1); + break; + + case EditOp::Remove: + replace1.pos1 = op.pos1; + replace1.length1 = op.length1; + replaceList->append(replace1); + break; + + case EditOp::Flip: + replace1.pos1 = op.pos1; + replace1.length1 = op.length1; + replace1.text = textAt(op.pos2, op.length2); + replaceList->append(replace1); + + replace2.pos1 = op.pos2; + replace2.length1 = op.length2; + replace2.text = textAt(op.pos1, op.length1); + replaceList->append(replace2); + break; + + case EditOp::Copy: + replace1.pos1 = op.pos2; + replace1.text = textAt(op.pos1, op.length1); + replaceList->append(replace1); + break; + + case EditOp::Unset: + break; + } +} + +bool ChangeSet::hadErrors() +{ + return m_error; +} + +void ChangeSet::apply(QString *s) +{ + m_string = s; + apply_helper(); + m_string = 0; +} + +void ChangeSet::apply(QTextCursor *textCursor) +{ + m_cursor = textCursor; + apply_helper(); + m_cursor = 0; +} + +QString ChangeSet::textAt(int pos, int length) +{ + if (m_string) { + return m_string->mid(pos, length); + } else if (m_cursor) { + m_cursor->setPosition(pos); + m_cursor->setPosition(pos + length, QTextCursor::KeepAnchor); + return m_cursor->selectedText(); + } + return QString(); +} + +void ChangeSet::apply_helper() +{ + // convert all ops to replace + QList<EditOp> replaceList; + { + while (!m_operationList.isEmpty()) { + const EditOp cmd(m_operationList.first()); + m_operationList.removeFirst(); + convertToReplace(cmd, &replaceList); + } + } + + // execute replaces + if (m_cursor) + m_cursor->beginEditBlock(); + + while (!replaceList.isEmpty()) { + const EditOp cmd(replaceList.first()); + replaceList.removeFirst(); + doReplace(cmd, &replaceList); + } + + if (m_cursor) + m_cursor->endEditBlock(); +} + +} // namespace Internal { + diff --git a/src/lib/api/changeset.h b/src/lib/api/changeset.h new file mode 100644 index 000000000..97f04fa7a --- /dev/null +++ b/src/lib/api/changeset.h @@ -0,0 +1,130 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt Build Suite. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ + +#ifndef QBS_CHANGESET_H +#define QBS_CHANGESET_H + +#include <QString> +#include <QList> + +QT_FORWARD_DECLARE_CLASS(QTextCursor) + +namespace QbsQmlJS { + +class ChangeSet +{ +public: + struct EditOp { + enum Type + { + Unset, + Replace, + Move, + Insert, + Remove, + Flip, + Copy + }; + + EditOp(): type(Unset), pos1(0), pos2(0), length1(0), length2(0) {} + EditOp(Type t): type(t), pos1(0), pos2(0), length1(0), length2(0) {} + + Type type; + int pos1; + int pos2; + int length1; + int length2; + QString text; + }; + + struct Range { + Range() + : start(0), end(0) {} + + Range(int start, int end) + : start(start), end(end) {} + + int start; + int end; + }; + +public: + ChangeSet(); + ChangeSet(const QList<EditOp> &operations); + + bool isEmpty() const; + + QList<EditOp> operationList() const; + + void clear(); + + bool replace(const Range &range, const QString &replacement); + bool remove(const Range &range); + bool move(const Range &range, int to); + bool flip(const Range &range1, const Range &range2); + bool copy(const Range &range, int to); + bool replace(int start, int end, const QString &replacement); + bool remove(int start, int end); + bool move(int start, int end, int to); + bool flip(int start1, int end1, int start2, int end2); + bool copy(int start, int end, int to); + bool insert(int pos, const QString &text); + + bool hadErrors(); + + void apply(QString *s); + void apply(QTextCursor *textCursor); + +private: + // length-based API. + bool replace_helper(int pos, int length, const QString &replacement); + bool move_helper(int pos, int length, int to); + bool remove_helper(int pos, int length); + bool flip_helper(int pos1, int length1, int pos2, int length2); + bool copy_helper(int pos, int length, int to); + + bool hasOverlap(int pos, int length); + QString textAt(int pos, int length); + + void doReplace(const EditOp &replace, QList<EditOp> *replaceList); + void convertToReplace(const EditOp &op, QList<EditOp> *replaceList); + + void apply_helper(); + +private: + QString *m_string; + QTextCursor *m_cursor; + + QList<EditOp> m_operationList; + bool m_error; +}; + +} // namespace QbsQmlJS + +#endif // Include guard. diff --git a/src/lib/api/qmljsrewriter.cpp b/src/lib/api/qmljsrewriter.cpp new file mode 100644 index 000000000..adea2d866 --- /dev/null +++ b/src/lib/api/qmljsrewriter.cpp @@ -0,0 +1,718 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt Build Suite. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ + +#include "qmljsrewriter.h" + +#include <parser/qmljsast_p.h> + +#include <QTextBlock> +#include <QTextCursor> +#include <QTextDocument> + +namespace QbsQmlJS { +using namespace AST; + +static QString toString(UiQualifiedId *qualifiedId, QChar delimiter = QLatin1Char('.')) +{ + QString result; + + for (UiQualifiedId *iter = qualifiedId; iter; iter = iter->next) { + if (iter != qualifiedId) + result += delimiter; + + result += iter->name; + } + + return result; +} + + +Rewriter::Rewriter(const QString &originalText, + ChangeSet *changeSet, + const QStringList &propertyOrder) + : m_originalText(originalText) + , m_changeSet(changeSet) + , m_propertyOrder(propertyOrder) +{ + Q_ASSERT(changeSet); +} + +Rewriter::Range Rewriter::addBinding(AST::UiObjectInitializer *ast, + const QString &propertyName, + const QString &propertyValue, + BindingType bindingType) +{ + UiObjectMemberList *insertAfter = searchMemberToInsertAfter(ast->members, + propertyName, + m_propertyOrder); + return addBinding(ast, propertyName, propertyValue, bindingType, insertAfter); +} + +Rewriter::Range Rewriter::addBinding(AST::UiObjectInitializer *ast, + const QString &propertyName, + const QString &propertyValue, + BindingType bindingType, + UiObjectMemberList *insertAfter) +{ + SourceLocation endOfPreviousMember; + SourceLocation startOfNextMember; + + if (insertAfter == 0 || insertAfter->member == 0) { + // insert as first member + endOfPreviousMember = ast->lbraceToken; + + if (ast->members && ast->members->member) + startOfNextMember = ast->members->member->firstSourceLocation(); + else + startOfNextMember = ast->rbraceToken; + } else { + endOfPreviousMember = insertAfter->member->lastSourceLocation(); + + if (insertAfter->next && insertAfter->next->member) + startOfNextMember = insertAfter->next->member->firstSourceLocation(); + else + startOfNextMember = ast->rbraceToken; + } + const bool isOneLiner = endOfPreviousMember.startLine == startOfNextMember.startLine; + bool needsPreceedingSemicolon = false; + bool needsTrailingSemicolon = false; + + if (isOneLiner) { + if (insertAfter == 0) { // we're inserting after an lbrace + if (ast->members) { // we're inserting before a member (and not the rbrace) + needsTrailingSemicolon = bindingType == ScriptBinding; + } + } else { // we're inserting after a member, not after the lbrace + if (endOfPreviousMember.isValid()) { // there already is a semicolon after the previous member + if (insertAfter->next && insertAfter->next->member) { // and the after us there is a member, not an rbrace, so: + needsTrailingSemicolon = bindingType == ScriptBinding; + } + } else { // there is no semicolon after the previous member (probably because there is an rbrace after us/it, so: + needsPreceedingSemicolon = true; + } + } + } + + QString newPropertyTemplate; + switch (bindingType) { + case ArrayBinding: + newPropertyTemplate = QLatin1String("%1: [\n%2\n]"); + break; + + case ObjectBinding: + newPropertyTemplate = QLatin1String("%1: %2"); + break; + + case ScriptBinding: + newPropertyTemplate = QLatin1String("%1: %2"); + break; + + default: + Q_ASSERT(!"unknown property type"); + } + + if (isOneLiner) { + if (needsPreceedingSemicolon) + newPropertyTemplate.prepend(QLatin1Char(';')); + newPropertyTemplate.prepend(QLatin1Char(' ')); + if (needsTrailingSemicolon) + newPropertyTemplate.append(QLatin1Char(';')); + } else { + newPropertyTemplate.prepend(QLatin1Char('\n')); + } + + m_changeSet->insert(endOfPreviousMember.end(), + newPropertyTemplate.arg(propertyName, propertyValue)); + + return Range(endOfPreviousMember.end(), endOfPreviousMember.end()); +} + +UiObjectMemberList *Rewriter::searchMemberToInsertAfter(UiObjectMemberList *members, + const QStringList &propertyOrder) +{ + const int objectDefinitionInsertionPoint = propertyOrder.indexOf(QString()); + + UiObjectMemberList *lastObjectDef = 0; + UiObjectMemberList *lastNonObjectDef = 0; + + for (UiObjectMemberList *iter = members; iter; iter = iter->next) { + UiObjectMember *member = iter->member; + int idx = -1; + + if (cast<UiObjectDefinition*>(member)) + lastObjectDef = iter; + else if (UiArrayBinding *arrayBinding = cast<UiArrayBinding*>(member)) + idx = propertyOrder.indexOf(toString(arrayBinding->qualifiedId)); + else if (UiObjectBinding *objectBinding = cast<UiObjectBinding*>(member)) + idx = propertyOrder.indexOf(toString(objectBinding->qualifiedId)); + else if (UiScriptBinding *scriptBinding = cast<UiScriptBinding*>(member)) + idx = propertyOrder.indexOf(toString(scriptBinding->qualifiedId)); + else if (cast<UiPublicMember*>(member)) + idx = propertyOrder.indexOf(QLatin1String("property")); + + if (idx < objectDefinitionInsertionPoint) + lastNonObjectDef = iter; + } + + if (lastObjectDef) + return lastObjectDef; + else + return lastNonObjectDef; +} + +UiArrayMemberList *Rewriter::searchMemberToInsertAfter(UiArrayMemberList *members, + const QStringList &propertyOrder) +{ + const int objectDefinitionInsertionPoint = propertyOrder.indexOf(QString()); + + UiArrayMemberList *lastObjectDef = 0; + UiArrayMemberList *lastNonObjectDef = 0; + + for (UiArrayMemberList *iter = members; iter; iter = iter->next) { + UiObjectMember *member = iter->member; + int idx = -1; + + if (cast<UiObjectDefinition*>(member)) + lastObjectDef = iter; + else if (UiArrayBinding *arrayBinding = cast<UiArrayBinding*>(member)) + idx = propertyOrder.indexOf(toString(arrayBinding->qualifiedId)); + else if (UiObjectBinding *objectBinding = cast<UiObjectBinding*>(member)) + idx = propertyOrder.indexOf(toString(objectBinding->qualifiedId)); + else if (UiScriptBinding *scriptBinding = cast<UiScriptBinding*>(member)) + idx = propertyOrder.indexOf(toString(scriptBinding->qualifiedId)); + else if (cast<UiPublicMember*>(member)) + idx = propertyOrder.indexOf(QLatin1String("property")); + + if (idx < objectDefinitionInsertionPoint) + lastNonObjectDef = iter; + } + + if (lastObjectDef) + return lastObjectDef; + else + return lastNonObjectDef; +} + +UiObjectMemberList *Rewriter::searchMemberToInsertAfter(UiObjectMemberList *members, + const QString &propertyName, + const QStringList &propertyOrder) +{ + if (!members) + return 0; // empty members + + QHash<QString, UiObjectMemberList *> orderedMembers; + + for (UiObjectMemberList *iter = members; iter; iter = iter->next) { + UiObjectMember *member = iter->member; + + if (UiArrayBinding *arrayBinding = cast<UiArrayBinding*>(member)) + orderedMembers[toString(arrayBinding->qualifiedId)] = iter; + else if (UiObjectBinding *objectBinding = cast<UiObjectBinding*>(member)) + orderedMembers[toString(objectBinding->qualifiedId)] = iter; + else if (cast<UiObjectDefinition*>(member)) + orderedMembers[QString::null] = iter; + else if (UiScriptBinding *scriptBinding = cast<UiScriptBinding*>(member)) + orderedMembers[toString(scriptBinding->qualifiedId)] = iter; + else if (cast<UiPublicMember*>(member)) + orderedMembers[QLatin1String("property")] = iter; + } + + int idx = propertyOrder.indexOf(propertyName); + if (idx == -1) + idx = propertyOrder.indexOf(QString()); + if (idx == -1) + idx = propertyOrder.size() - 1; + + for (; idx > 0; --idx) { + const QString prop = propertyOrder.at(idx - 1); + UiObjectMemberList *candidate = orderedMembers.value(prop, 0); + if (candidate != 0) + return candidate; + } + + return 0; +} + +void Rewriter::changeBinding(UiObjectInitializer *ast, + const QString &propertyName, + const QString &newValue, + BindingType binding) +{ + QString prefix, suffix; + int dotIdx = propertyName.indexOf(QLatin1Char('.')); + if (dotIdx != -1) { + prefix = propertyName.left(dotIdx); + suffix = propertyName.mid(dotIdx + 1); + } + + for (UiObjectMemberList *members = ast->members; members; members = members->next) { + UiObjectMember *member = members->member; + + // for non-grouped properties: + if (isMatchingPropertyMember(propertyName, member)) { + switch (binding) { + case ArrayBinding: + insertIntoArray(cast<UiArrayBinding*>(member), newValue); + break; + + case ObjectBinding: + replaceMemberValue(member, newValue, false); + break; + + case ScriptBinding: + replaceMemberValue(member, newValue, nextMemberOnSameLine(members)); + break; + + default: + Q_ASSERT(!"Unhandled QmlRefactoring::PropertyType"); + } + + break; + // for grouped properties: + } else if (!prefix.isEmpty()) { + if (UiObjectDefinition *def = cast<UiObjectDefinition *>(member)) { + if (toString(def->qualifiedTypeNameId) == prefix) + changeBinding(def->initializer, suffix, newValue, binding); + } + } + } +} + +void Rewriter::replaceMemberValue(UiObjectMember *propertyMember, + const QString &newValue, + bool needsSemicolon) +{ + QString replacement = newValue; + int startOffset = -1; + int endOffset = -1; + if (UiObjectBinding *objectBinding = AST::cast<UiObjectBinding *>(propertyMember)) { + startOffset = objectBinding->qualifiedTypeNameId->identifierToken.offset; + endOffset = objectBinding->initializer->rbraceToken.end(); + } else if (UiScriptBinding *scriptBinding = AST::cast<UiScriptBinding *>(propertyMember)) { + startOffset = scriptBinding->statement->firstSourceLocation().offset; + endOffset = scriptBinding->statement->lastSourceLocation().end(); + } else if (UiArrayBinding *arrayBinding = AST::cast<UiArrayBinding *>(propertyMember)) { + startOffset = arrayBinding->lbracketToken.offset; + endOffset = arrayBinding->rbracketToken.end(); + } else if (UiPublicMember *publicMember = AST::cast<UiPublicMember*>(propertyMember)) { + if (publicMember->statement) { + startOffset = publicMember->statement->firstSourceLocation().offset; + if (publicMember->semicolonToken.isValid()) + endOffset = publicMember->semicolonToken.end(); + else + endOffset = publicMember->statement->lastSourceLocation().offset; + } else { + startOffset = publicMember->lastSourceLocation().end(); + endOffset = startOffset; + if (publicMember->semicolonToken.isValid()) + startOffset = publicMember->semicolonToken.offset; + replacement.prepend(QLatin1String(": ")); + } + } else { + return; + } + + if (needsSemicolon) + replacement += QLatin1Char(';'); + + m_changeSet->replace(startOffset, endOffset, replacement); +} + +bool Rewriter::isMatchingPropertyMember(const QString &propertyName, + UiObjectMember *member) +{ + if (UiPublicMember *publicMember = cast<UiPublicMember*>(member)) + return publicMember->name == propertyName; + else if (UiObjectBinding *objectBinding = cast<UiObjectBinding*>(member)) + return toString(objectBinding->qualifiedId) == propertyName; + else if (UiScriptBinding *scriptBinding = cast<UiScriptBinding*>(member)) + return toString(scriptBinding->qualifiedId) == propertyName; + else if (UiArrayBinding *arrayBinding = cast<UiArrayBinding*>(member)) + return toString(arrayBinding->qualifiedId) == propertyName; + else + return false; +} + +bool Rewriter::nextMemberOnSameLine(UiObjectMemberList *members) +{ + if (members && members->next && members->next->member) + return members->next->member->firstSourceLocation().startLine == members->member->lastSourceLocation().startLine; + else + return false; +} + +void Rewriter::insertIntoArray(UiArrayBinding *ast, const QString &newValue) +{ + if (!ast) + return; + + UiObjectMember *lastMember = 0; + for (UiArrayMemberList *iter = ast->members; iter; iter = iter->next) { + lastMember = iter->member; + } + + if (!lastMember) + return; + + const int insertionPoint = lastMember->lastSourceLocation().end(); + m_changeSet->insert(insertionPoint, QLatin1String(",\n") + newValue); +} + +void Rewriter::removeBindingByName(UiObjectInitializer *ast, const QString &propertyName) +{ + QString prefix; + int dotIdx = propertyName.indexOf(QLatin1Char('.')); + if (dotIdx != -1) + prefix = propertyName.left(dotIdx); + + for (UiObjectMemberList *it = ast->members; it; it = it->next) { + UiObjectMember *member = it->member; + + // run full name match (for ungrouped properties): + if (isMatchingPropertyMember(propertyName, member)) { + removeMember(member); + // check for grouped properties: + } else if (!prefix.isEmpty()) { + if (UiObjectDefinition *def = cast<UiObjectDefinition *>(member)) { + if (toString(def->qualifiedTypeNameId) == prefix) + removeGroupedProperty(def, propertyName); + } + } + } +} + +void Rewriter::removeGroupedProperty(UiObjectDefinition *ast, + const QString &propertyName) +{ + int dotIdx = propertyName.indexOf(QLatin1Char('.')); + if (dotIdx == -1) + return; + + const QString propName = propertyName.mid(dotIdx + 1); + + UiObjectMember *wanted = 0; + unsigned memberCount = 0; + for (UiObjectMemberList *it = ast->initializer->members; it; it = it->next) { + ++memberCount; + UiObjectMember *member = it->member; + + if (!wanted && isMatchingPropertyMember(propName, member)) + wanted = member; + } + + if (!wanted) + return; + if (memberCount == 1) + removeMember(ast); + else + removeMember(wanted); +} + +void Rewriter::removeMember(UiObjectMember *member) +{ + int start = member->firstSourceLocation().offset; + int end = member->lastSourceLocation().end(); + + includeSurroundingWhitespace(m_originalText, start, end); + + m_changeSet->remove(start, end); +} + +bool Rewriter::includeSurroundingWhitespace(const QString &source, int &start, int &end) +{ + bool includeStartingWhitespace = true; + bool paragraphFound = false; + bool paragraphSkipped = false; + + if (end >= 0) { + QChar c = source.at(end); + + while (c.isSpace()) { + ++end; + if (c.unicode() == 10) { + paragraphFound = true; + paragraphSkipped = true; + break; + } else if (end == source.length()) { + break; + } + + c = source.at(end); + } + + includeStartingWhitespace = paragraphFound; + } + + paragraphFound = false; + if (includeStartingWhitespace) { + while (start > 0) { + const QChar c = source.at(start - 1); + + if (c.unicode() == 10) { + paragraphFound = true; + break; + } + if (!c.isSpace()) + break; + + --start; + } + } + if (!paragraphFound && paragraphSkipped) //keep the line ending + --end; + + return paragraphFound; +} + +void Rewriter::includeLeadingEmptyLine(const QString &source, int &start) +{ + QTextDocument doc(source); + + if (start == 0) + return; + + if (doc.characterAt(start - 1) != QChar::ParagraphSeparator) + return; + + QTextCursor tc(&doc); + tc.setPosition(start); + const int blockNr = tc.blockNumber(); + if (blockNr == 0) + return; + + const QTextBlock prevBlock = tc.block().previous(); + const QString trimmedPrevBlockText = prevBlock.text().trimmed(); + if (!trimmedPrevBlockText.isEmpty()) + return; + + start = prevBlock.position(); +} + +void Rewriter::includeEmptyGroupedProperty(UiObjectDefinition *groupedProperty, UiObjectMember *memberToBeRemoved, int &start, int &end) +{ + if (groupedProperty->qualifiedTypeNameId && !groupedProperty->qualifiedTypeNameId->name.isEmpty() + && groupedProperty->qualifiedTypeNameId->name.at(0).isLower()) { + // grouped property + UiObjectMemberList *memberIter = groupedProperty->initializer->members; + while (memberIter) { + if (memberIter->member != memberToBeRemoved) + return; + memberIter = memberIter->next; + } + start = groupedProperty->firstSourceLocation().begin(); + end = groupedProperty->lastSourceLocation().end(); + } +} + +#if 0 +UiObjectMemberList *QMLRewriter::searchMemberToInsertAfter(UiObjectMemberList *members, const QStringList &propertyOrder) +{ + const int objectDefinitionInsertionPoint = propertyOrder.indexOf(QString::null); + + UiObjectMemberList *lastObjectDef = 0; + UiObjectMemberList *lastNonObjectDef = 0; + + for (UiObjectMemberList *iter = members; iter; iter = iter->next) { + UiObjectMember *member = iter->member; + int idx = -1; + + if (cast<UiObjectDefinition*>(member)) + lastObjectDef = iter; + else if (UiArrayBinding *arrayBinding = cast<UiArrayBinding*>(member)) + idx = propertyOrder.indexOf(toString(arrayBinding->qualifiedId)); + else if (UiObjectBinding *objectBinding = cast<UiObjectBinding*>(member)) + idx = propertyOrder.indexOf(toString(objectBinding->qualifiedId)); + else if (UiScriptBinding *scriptBinding = cast<UiScriptBinding*>(member)) + idx = propertyOrder.indexOf(toString(scriptBinding->qualifiedId)); + else if (cast<UiPublicMember*>(member)) + idx = propertyOrder.indexOf(QLatin1String("property")); + + if (idx < objectDefinitionInsertionPoint) + lastNonObjectDef = iter; + } + + if (lastObjectDef) + return lastObjectDef; + else + return lastNonObjectDef; +} + +UiObjectMemberList *QMLRewriter::searchMemberToInsertAfter(UiObjectMemberList *members, const QString &propertyName, const QStringList &propertyOrder) +{ + if (!members) + return 0; // empty members + + QHash<QString, UiObjectMemberList *> orderedMembers; + + for (UiObjectMemberList *iter = members; iter; iter = iter->next) { + UiObjectMember *member = iter->member; + + if (UiArrayBinding *arrayBinding = cast<UiArrayBinding*>(member)) + orderedMembers[toString(arrayBinding->qualifiedId)] = iter; + else if (UiObjectBinding *objectBinding = cast<UiObjectBinding*>(member)) + orderedMembers[toString(objectBinding->qualifiedId)] = iter; + else if (cast<UiObjectDefinition*>(member)) + orderedMembers[QString::null] = iter; + else if (UiScriptBinding *scriptBinding = cast<UiScriptBinding*>(member)) + orderedMembers[toString(scriptBinding->qualifiedId)] = iter; + else if (cast<UiPublicMember*>(member)) + orderedMembers[QLatin1String("property")] = iter; + } + + int idx = propertyOrder.indexOf(propertyName); + if (idx == -1) + idx = propertyOrder.indexOf(QString()); + if (idx == -1) + idx = propertyOrder.size() - 1; + + for (; idx > 0; --idx) { + const QString prop = propertyOrder.at(idx - 1); + UiObjectMemberList *candidate = orderedMembers.value(prop, 0); + if (candidate != 0) + return candidate; + } + + return 0; +} + +#endif + +void Rewriter::appendToArrayBinding(UiArrayBinding *arrayBinding, + const QString &content) +{ + UiObjectMember *lastMember = 0; + for (UiArrayMemberList *iter = arrayBinding->members; iter; iter = iter->next) + if (iter->member) + lastMember = iter->member; + + if (!lastMember) + return; // an array binding cannot be empty, so there will (or should) always be a last member. + + const int insertionPoint = lastMember->lastSourceLocation().end(); + + m_changeSet->insert(insertionPoint, QLatin1String(",\n") + content); +} + +Rewriter::Range Rewriter::addObject(UiObjectInitializer *ast, const QString &content) +{ + UiObjectMemberList *insertAfter = searchMemberToInsertAfter(ast->members, m_propertyOrder); + return addObject(ast, content, insertAfter); +} + +Rewriter::Range Rewriter::addObject(UiObjectInitializer *ast, const QString &content, UiObjectMemberList *insertAfter) +{ + int insertionPoint; + QString textToInsert; + if (insertAfter && insertAfter->member) { + insertionPoint = insertAfter->member->lastSourceLocation().end(); + textToInsert += QLatin1String("\n"); + } else { + insertionPoint = ast->lbraceToken.end(); + } + + textToInsert += content; + m_changeSet->insert(insertionPoint, QLatin1String("\n") + textToInsert); + + return Range(insertionPoint, insertionPoint); +} + +Rewriter::Range Rewriter::addObject(UiArrayBinding *ast, const QString &content) +{ + UiArrayMemberList *insertAfter = searchMemberToInsertAfter(ast->members, m_propertyOrder); + return addObject(ast, content, insertAfter); +} + +Rewriter::Range Rewriter::addObject(UiArrayBinding *ast, const QString &content, UiArrayMemberList *insertAfter) +{ + int insertionPoint; + QString textToInsert; + if (insertAfter && insertAfter->member) { + insertionPoint = insertAfter->member->lastSourceLocation().end(); + textToInsert = QLatin1String(",\n") + content; + } else { + insertionPoint = ast->lbracketToken.end(); + textToInsert += QLatin1String("\n") + content + QLatin1Char(','); + } + + m_changeSet->insert(insertionPoint, textToInsert); + + return Range(insertionPoint, insertionPoint); +} + +void Rewriter::removeObjectMember(UiObjectMember *member, UiObjectMember *parent) +{ + int start = member->firstSourceLocation().offset; + int end = member->lastSourceLocation().end(); + + if (UiArrayBinding *parentArray = cast<UiArrayBinding *>(parent)) { + extendToLeadingOrTrailingComma(parentArray, member, start, end); + } else { + if (UiObjectDefinition *parentObjectDefinition = cast<UiObjectDefinition *>(parent)) + includeEmptyGroupedProperty(parentObjectDefinition, member, start, end); + includeSurroundingWhitespace(m_originalText, start, end); + } + + includeLeadingEmptyLine(m_originalText, start); + m_changeSet->remove(start, end); +} + +void Rewriter::extendToLeadingOrTrailingComma(UiArrayBinding *parentArray, + UiObjectMember *member, + int &start, + int &end) const +{ + UiArrayMemberList *currentMember = 0; + for (UiArrayMemberList *it = parentArray->members; it; it = it->next) { + if (it->member == member) { + currentMember = it; + break; + } + } + + if (!currentMember) + return; + + if (currentMember->commaToken.isValid()) { + // leading comma + start = currentMember->commaToken.offset; + if (includeSurroundingWhitespace(m_originalText, start, end)) + --end; + } else if (currentMember->next && currentMember->next->commaToken.isValid()) { + // trailing comma + end = currentMember->next->commaToken.end(); + includeSurroundingWhitespace(m_originalText, start, end); + } else { + // array with 1 element, so remove the complete binding + start = parentArray->firstSourceLocation().offset; + end = parentArray->lastSourceLocation().end(); + includeSurroundingWhitespace(m_originalText, start, end); + } +} + +} // namespace QbsQmlJS diff --git a/src/lib/api/qmljsrewriter.h b/src/lib/api/qmljsrewriter.h new file mode 100644 index 000000000..dcdb28c28 --- /dev/null +++ b/src/lib/api/qmljsrewriter.h @@ -0,0 +1,120 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt Build Suite. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +****************************************************************************/ + +#ifndef QMLJSREWRITER_H +#define QMLJSREWRITER_H + +#include "changeset.h" + +#include <parser/qmljsastfwd_p.h> + +#include <QStringList> + +namespace QbsQmlJS { + +class Rewriter +{ +public: + enum BindingType { + ScriptBinding, + ObjectBinding, + ArrayBinding + }; + + typedef ChangeSet::Range Range; + +public: + Rewriter(const QString &originalText, + ChangeSet *changeSet, + const QStringList &propertyOrder); + + Range addBinding(AST::UiObjectInitializer *ast, + const QString &propertyName, + const QString &propertyValue, + BindingType bindingType); + + Range addBinding(AST::UiObjectInitializer *ast, + const QString &propertyName, + const QString &propertyValue, + BindingType bindingType, + AST::UiObjectMemberList *insertAfter); + + void changeBinding(AST::UiObjectInitializer *ast, + const QString &propertyName, + const QString &newValue, + BindingType binding); + + void removeBindingByName(AST::UiObjectInitializer *ast, const QString &propertyName); + + void appendToArrayBinding(AST::UiArrayBinding *arrayBinding, + const QString &content); + + Range addObject(AST::UiObjectInitializer *ast, const QString &content); + Range addObject(AST::UiObjectInitializer *ast, const QString &content, AST::UiObjectMemberList *insertAfter); + Range addObject(AST::UiArrayBinding *ast, const QString &content); + Range addObject(AST::UiArrayBinding *ast, const QString &content, AST::UiArrayMemberList *insertAfter); + + void removeObjectMember(AST::UiObjectMember *member, AST::UiObjectMember *parent); + + static AST::UiObjectMemberList *searchMemberToInsertAfter(AST::UiObjectMemberList *members, const QStringList &propertyOrder); + static AST::UiArrayMemberList *searchMemberToInsertAfter(AST::UiArrayMemberList *members, const QStringList &propertyOrder); + static AST::UiObjectMemberList *searchMemberToInsertAfter(AST::UiObjectMemberList *members, const QString &propertyName, const QStringList &propertyOrder); + + static bool includeSurroundingWhitespace(const QString &source, int &start, int &end); + static void includeLeadingEmptyLine(const QString &source, int &start); + static void includeEmptyGroupedProperty(AST::UiObjectDefinition *groupedProperty, AST::UiObjectMember *memberToBeRemoved, int &start, int &end); + +private: + void replaceMemberValue(AST::UiObjectMember *propertyMember, + const QString &newValue, + bool needsSemicolon); + static bool isMatchingPropertyMember(const QString &propertyName, + AST::UiObjectMember *member); + static bool nextMemberOnSameLine(AST::UiObjectMemberList *members); + + void insertIntoArray(AST::UiArrayBinding* ast, const QString &newValue); + + void removeMember(AST::UiObjectMember *member); + void removeGroupedProperty(AST::UiObjectDefinition *ast, + const QString &propertyName); + + void extendToLeadingOrTrailingComma(AST::UiArrayBinding *parentArray, + AST::UiObjectMember *member, + int &start, + int &end) const; + +private: + QString m_originalText; + ChangeSet *m_changeSet; + const QStringList m_propertyOrder; +}; + +} // namespace QbsQmlJS + +#endif // QMLJSREWRITER_H diff --git a/src/lib/lib.pro b/src/lib/lib.pro index 14c24a083..27dee1c39 100644 --- a/src/lib/lib.pro +++ b/src/lib/lib.pro @@ -1,4 +1,4 @@ -QT = core script +QT = core script gui all_tests:QT += testlib TEMPLATE = lib diff --git a/src/lib/lib.qbs b/src/lib/lib.qbs index e30f142e9..c45997744 100644 --- a/src/lib/lib.qbs +++ b/src/lib/lib.qbs @@ -3,7 +3,7 @@ import "../../version.js" as Version Product { Depends { name: "cpp" } - Depends { name: "Qt"; submodules: ["core", "script", "xml"] } + Depends { name: "Qt"; submodules: ["core", "gui", "script", "xml"] } Depends { condition: project.enableUnitTests; name: "Qt.test" } name: "qbscore" version: Version.qbsVersion() @@ -41,12 +41,16 @@ Product { name: "api" prefix: name + '/' files: [ + "changeset.cpp", + "changeset.h", "internaljobs.cpp", "internaljobs.h", "jobs.cpp", "project.cpp", "projectdata.cpp", "projectdata_p.h", + "qmljsrewriter.cpp", + "qmljsrewriter.h", "propertymap_p.h", "runenvironment.cpp", ] |
