/************************************************************************** ** ** This file is part of Qt Creator ** ** Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies). ** ** Contact: Nokia Corporation (qt-info@nokia.com) ** ** Commercial Usage ** ** Licensees holding valid Qt Commercial licenses may use this file in ** accordance with the Qt Commercial License Agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and Nokia. ** ** 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. ** ** If you are unsure which license is appropriate for your use, please ** contact the sales department at http://qt.nokia.com/contact. ** **************************************************************************/ #include "qmljseditor.h" #include "qmljseditorconstants.h" #include "qmlhighlighter.h" #include "qmljseditorplugin.h" #include "qmlmodelmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include enum { UPDATE_DOCUMENT_DEFAULT_INTERVAL = 50, UPDATE_USES_DEFAULT_INTERVAL = 150 }; using namespace QmlJS; using namespace QmlJS::AST; using namespace QmlJSEditor::Internal; namespace { int blockBraceDepth(const QTextBlock &block) { int state = block.userState(); if (state == -1) return 0; return (state >> 8) & 0xFF; } int blockStartState(const QTextBlock &block) { int state = block.userState(); if (state == -1) return 0; else return state & 0xff; } bool shouldInsertMatchingText(const QChar &lookAhead) { switch (lookAhead.unicode()) { case '{': case '}': case ']': case ')': case ';': case ',': case '"': case '\'': return true; default: if (lookAhead.isSpace()) return true; return false; } // switch } bool shouldInsertMatchingText(const QTextCursor &tc) { QTextDocument *doc = tc.document(); return shouldInsertMatchingText(doc->characterAt(tc.selectionEnd())); } class FindIdDeclarations: protected Visitor { public: typedef QHash > Result; Result operator()(Document::Ptr doc) { _ids.clear(); _maybeIds.clear(); if (doc && doc->qmlProgram()) doc->qmlProgram()->accept(this); return _ids; } protected: QString asString(AST::UiQualifiedId *id) { QString text; for (; id; id = id->next) { if (id->name) text += id->name->asString(); else text += QLatin1Char('?'); if (id->next) text += QLatin1Char('.'); } return text; } void accept(AST::Node *node) { AST::Node::acceptChild(node, this); } using Visitor::visit; using Visitor::endVisit; virtual bool visit(AST::UiScriptBinding *node) { if (asString(node->qualifiedId) == QLatin1String("id")) { if (AST::ExpressionStatement *stmt = AST::cast(node->statement)) { if (AST::IdentifierExpression *idExpr = AST::cast(stmt->expression)) { if (idExpr->name) { const QString id = idExpr->name->asString(); QList *locs = &_ids[id]; locs->append(idExpr->firstSourceLocation()); locs->append(_maybeIds.value(id)); _maybeIds.remove(id); return false; } } } } accept(node->statement); return false; } virtual bool visit(AST::IdentifierExpression *node) { if (node->name) { const QString name = node->name->asString(); if (_ids.contains(name)) _ids[name].append(node->identifierToken); else _maybeIds[name].append(node->identifierToken); } return false; } private: Result _ids; Result _maybeIds; }; class FindDeclarations: protected Visitor { QList _declarations; int _depth; public: QList operator()(AST::Node *node) { _depth = -1; _declarations.clear(); accept(node); return _declarations; } protected: using Visitor::visit; using Visitor::endVisit; QString asString(AST::UiQualifiedId *id) { QString text; for (; id; id = id->next) { if (id->name) text += id->name->asString(); else text += QLatin1Char('?'); if (id->next) text += QLatin1Char('.'); } return text; } void accept(AST::Node *node) { AST::Node::acceptChild(node, this); } void init(Declaration *decl, AST::UiObjectMember *member) { const SourceLocation first = member->firstSourceLocation(); const SourceLocation last = member->lastSourceLocation(); decl->startLine = first.startLine; decl->startColumn = first.startColumn; decl->endLine = last.startLine; decl->endColumn = last.startColumn + last.length; } void init(Declaration *decl, AST::ExpressionNode *expressionNode) { const SourceLocation first = expressionNode->firstSourceLocation(); const SourceLocation last = expressionNode->lastSourceLocation(); decl->startLine = first.startLine; decl->startColumn = first.startColumn; decl->endLine = last.startLine; decl->endColumn = last.startColumn + last.length; } virtual bool visit(AST::UiObjectDefinition *node) { ++_depth; Declaration decl; init(&decl, node); decl.text.fill(QLatin1Char(' '), _depth); if (node->qualifiedTypeNameId) decl.text.append(asString(node->qualifiedTypeNameId)); else decl.text.append(QLatin1Char('?')); _declarations.append(decl); return true; // search for more bindings } virtual void endVisit(AST::UiObjectDefinition *) { --_depth; } virtual bool visit(AST::UiObjectBinding *node) { ++_depth; Declaration decl; init(&decl, node); decl.text.fill(QLatin1Char(' '), _depth); decl.text.append(asString(node->qualifiedId)); decl.text.append(QLatin1String(": ")); if (node->qualifiedTypeNameId) decl.text.append(asString(node->qualifiedTypeNameId)); else decl.text.append(QLatin1Char('?')); _declarations.append(decl); return true; // search for more bindings } virtual void endVisit(AST::UiObjectBinding *) { --_depth; } virtual bool visit(AST::UiScriptBinding *) { ++_depth; #if 0 // ### ignore script bindings for now. Declaration decl; init(&decl, node); decl.text.fill(QLatin1Char(' '), _depth); decl.text.append(asString(node->qualifiedId)); _declarations.append(decl); #endif return false; // more more bindings in this subtree. } virtual void endVisit(AST::UiScriptBinding *) { --_depth; } virtual bool visit(AST::FunctionExpression *) { return false; } virtual bool visit(AST::FunctionDeclaration *ast) { if (! ast->name) return false; Declaration decl; init(&decl, ast); decl.text.fill(QLatin1Char(' '), _depth); decl.text += ast->name->asString(); decl.text += QLatin1Char('('); for (FormalParameterList *it = ast->formals; it; it = it->next) { if (it->name) decl.text += it->name->asString(); if (it->next) decl.text += QLatin1String(", "); } decl.text += QLatin1Char(')'); _declarations.append(decl); return false; } virtual bool visit(AST::VariableDeclaration *ast) { if (! ast->name) return false; Declaration decl; decl.text.fill(QLatin1Char(' '), _depth); decl.text += ast->name->asString(); const SourceLocation first = ast->identifierToken; decl.startLine = first.startLine; decl.startColumn = first.startColumn; decl.endLine = first.startLine; decl.endColumn = first.startColumn + first.length; _declarations.append(decl); return false; } }; class CreateRanges: protected AST::Visitor { QTextDocument *_textDocument; QList _ranges; public: QList operator()(QTextDocument *textDocument, Document::Ptr doc) { _textDocument = textDocument; _ranges.clear(); if (doc && doc->qmlProgram() != 0) doc->qmlProgram()->accept(this); return _ranges; } protected: using AST::Visitor::visit; virtual bool visit(AST::UiObjectBinding *ast) { if (ast->initializer) _ranges.append(createRange(ast, ast->initializer)); return true; } virtual bool visit(AST::UiObjectDefinition *ast) { if (ast->initializer) _ranges.append(createRange(ast, ast->initializer)); return true; } Range createRange(AST::UiObjectMember *member, AST::UiObjectInitializer *ast) { Range range; range.ast = member; range.begin = QTextCursor(_textDocument); range.begin.setPosition(ast->lbraceToken.begin()); range.end = QTextCursor(_textDocument); range.end.setPosition(ast->rbraceToken.end()); return range; } }; class CollectASTNodes: protected AST::Visitor { public: QList qualifiedIds; QList identifiers; QList fieldMembers; void accept(AST::Node *node) { if (node) node->accept(this); } protected: using AST::Visitor::visit; virtual bool visit(AST::UiQualifiedId *ast) { qualifiedIds.append(ast); return false; } virtual bool visit(AST::IdentifierExpression *ast) { identifiers.append(ast); return false; } virtual bool visit(AST::FieldMemberExpression *ast) { fieldMembers.append(ast); return true; } }; } // end of anonymous namespace AST::UiObjectMember *SemanticInfo::declaringMember(int cursorPosition) const { AST::UiObjectMember *declaringMember = 0; for (int i = ranges.size() - 1; i != -1; --i) { const Range &range = ranges.at(i); if (range.begin.isNull() || range.end.isNull()) { continue; } else if (cursorPosition >= range.begin.position() && cursorPosition <= range.end.position()) { declaringMember = range.ast; break; } } return declaringMember; } AST::Node *SemanticInfo::nodeUnderCursor(int pos) const { if (! document) return 0; const unsigned cursorPosition = pos; CollectASTNodes nodes; nodes.accept(document->ast()); foreach (AST::UiQualifiedId *q, nodes.qualifiedIds) { if (cursorPosition >= q->identifierToken.begin()) { for (AST::UiQualifiedId *tail = q; tail; tail = tail->next) { if (! tail->next && cursorPosition <= tail->identifierToken.end()) return q; } } } foreach (AST::IdentifierExpression *id, nodes.identifiers) { if (cursorPosition >= id->identifierToken.begin() && cursorPosition <= id->identifierToken.end()) return id; } foreach (AST::FieldMemberExpression *mem, nodes.fieldMembers) { if (mem->name && cursorPosition >= mem->identifierToken.begin() && cursorPosition <= mem->identifierToken.end()) return mem; } return 0; } int SemanticInfo::revision() const { if (document) return document->documentRevision(); return 0; } QmlJSEditorEditable::QmlJSEditorEditable(QmlJSTextEditor *editor) : BaseTextEditorEditable(editor) { Core::UniqueIDManager *uidm = Core::UniqueIDManager::instance(); m_context << uidm->uniqueIdentifier(QmlJSEditor::Constants::C_QMLJSEDITOR_ID); m_context << uidm->uniqueIdentifier(TextEditor::Constants::C_TEXTEDITOR); } QmlJSTextEditor::QmlJSTextEditor(QWidget *parent) : TextEditor::BaseTextEditor(parent), m_methodCombo(0), m_modelManager(0) { setParenthesesMatchingEnabled(true); setMarksVisible(true); setCodeFoldingSupported(true); setCodeFoldingVisible(true); m_updateDocumentTimer = new QTimer(this); m_updateDocumentTimer->setInterval(UPDATE_DOCUMENT_DEFAULT_INTERVAL); m_updateDocumentTimer->setSingleShot(true); connect(m_updateDocumentTimer, SIGNAL(timeout()), this, SLOT(updateDocumentNow())); m_updateUsesTimer = new QTimer(this); m_updateUsesTimer->setInterval(UPDATE_USES_DEFAULT_INTERVAL); m_updateUsesTimer->setSingleShot(true); connect(m_updateUsesTimer, SIGNAL(timeout()), this, SLOT(updateUsesNow())); connect(this, SIGNAL(textChanged()), this, SLOT(updateDocument())); connect(this, SIGNAL(textChanged()), this, SLOT(updateUses())); baseTextDocument()->setSyntaxHighlighter(new QmlHighlighter); m_modelManager = ExtensionSystem::PluginManager::instance()->getObject(); if (m_modelManager) { connect(m_modelManager, SIGNAL(documentUpdated(QmlJS::Document::Ptr)), this, SLOT(onDocumentUpdated(QmlJS::Document::Ptr))); } } QmlJSTextEditor::~QmlJSTextEditor() { } Core::IEditor *QmlJSEditorEditable::duplicate(QWidget *parent) { QmlJSTextEditor *newEditor = new QmlJSTextEditor(parent); newEditor->duplicateFrom(editor()); QmlJSEditorPlugin::instance()->initializeEditor(newEditor); return newEditor->editableInterface(); } QString QmlJSEditorEditable::id() const { return QLatin1String(QmlJSEditor::Constants::C_QMLJSEDITOR_ID); } bool QmlJSEditorEditable::open(const QString &fileName) { bool b = TextEditor::BaseTextEditorEditable::open(fileName); editor()->setMimeType(Core::ICore::instance()->mimeDatabase()->findByFile(QFileInfo(fileName)).type()); return b; } QmlJSTextEditor::Context QmlJSEditorEditable::context() const { return m_context; } void QmlJSTextEditor::updateDocument() { m_updateDocumentTimer->start(UPDATE_DOCUMENT_DEFAULT_INTERVAL); } void QmlJSTextEditor::updateDocumentNow() { // ### move in the parser thread. m_updateDocumentTimer->stop(); const QString fileName = file()->fileName(); m_modelManager->updateSourceFiles(QStringList() << fileName); } void QmlJSTextEditor::onDocumentUpdated(QmlJS::Document::Ptr doc) { if (file()->fileName() != doc->fileName()) return; if (doc->documentRevision() != document()->revision()) { // got an outdated document. return; } if (doc->ast()) { // got a correctly parsed (or recovered) file. // create the ranges and update the semantic info. CreateRanges createRanges; SemanticInfo sem; sem.snapshot = m_modelManager->snapshot(); sem.document = doc; sem.ranges = createRanges(document(), doc); // Refresh the ids FindIdDeclarations updateIds; sem.idLocations = updateIds(doc); if (doc->isParsedCorrectly()) { FindDeclarations findDeclarations; sem.declarations = findDeclarations(doc->ast()); QStringList items; items.append(tr("