From 3f505e9982e2a07ce41e650d9ae547c436f12a43 Mon Sep 17 00:00:00 2001 From: Christian Kamm <christian.d.kamm@nokia.com> Date: Fri, 24 Sep 2010 14:05:34 +0200 Subject: [PATCH] QmlJS: Add initial 'Find Usages' support. --- src/libs/qmljs/qmljscheck.cpp | 2 +- src/libs/qmljs/qmljsinterpreter.cpp | 6 +- src/libs/qmljs/qmljsinterpreter.h | 2 +- src/libs/qmljs/qmljslink.cpp | 81 --- src/libs/qmljs/qmljslink.h | 8 - src/libs/qmljs/qmljslookupcontext.cpp | 4 +- src/libs/qmljs/qmljsmodelmanagerinterface.h | 26 + src/libs/qmljs/qmljsscopebuilder.cpp | 83 ++- src/libs/qmljs/qmljsscopebuilder.h | 14 +- .../designercore/model/texttomodelmerger.cpp | 2 +- src/plugins/qmljseditor/qmljseditor.cpp | 9 +- src/plugins/qmljseditor/qmljseditor.h | 4 + src/plugins/qmljseditor/qmljseditor.pro | 6 +- .../qmljseditor/qmljseditorconstants.h | 2 + src/plugins/qmljseditor/qmljseditorplugin.cpp | 14 + src/plugins/qmljseditor/qmljseditorplugin.h | 1 + .../qmljseditor/qmljsfindreferences.cpp | 685 ++++++++++++++++++ src/plugins/qmljseditor/qmljsfindreferences.h | 97 +++ src/plugins/qmljseditor/qmljsmodelmanager.cpp | 51 +- src/plugins/qmljseditor/qmljsmodelmanager.h | 12 +- 20 files changed, 967 insertions(+), 142 deletions(-) create mode 100644 src/plugins/qmljseditor/qmljsfindreferences.cpp create mode 100644 src/plugins/qmljseditor/qmljsfindreferences.h diff --git a/src/libs/qmljs/qmljscheck.cpp b/src/libs/qmljs/qmljscheck.cpp index bcab73dcb8e..2ab09a637cb 100644 --- a/src/libs/qmljs/qmljscheck.cpp +++ b/src/libs/qmljs/qmljscheck.cpp @@ -172,7 +172,7 @@ Check::Check(Document::Ptr doc, const Snapshot &snapshot, const Context *linkedC : _doc(doc) , _snapshot(snapshot) , _context(*linkedContextNoScope) - , _scopeBuilder(doc, &_context) + , _scopeBuilder(&_context, doc, snapshot) , _ignoreTypeErrors(false) { } diff --git a/src/libs/qmljs/qmljsinterpreter.cpp b/src/libs/qmljs/qmljsinterpreter.cpp index fa9e2e59e94..302a6df599b 100644 --- a/src/libs/qmljs/qmljsinterpreter.cpp +++ b/src/libs/qmljs/qmljsinterpreter.cpp @@ -1424,17 +1424,21 @@ void Context::setTypeEnvironment(const QmlJS::Document *doc, const TypeEnvironme _typeEnvironments[doc->fileName()] = typeEnvironment; } -const Value *Context::lookup(const QString &name) const +const Value *Context::lookup(const QString &name, const ObjectValue **foundInScope) const { QList<const ObjectValue *> scopes = _scopeChain.all(); for (int index = scopes.size() - 1; index != -1; --index) { const ObjectValue *scope = scopes.at(index); if (const Value *member = scope->lookupMember(name, this)) { + if (foundInScope) + *foundInScope = scope; return member; } } + if (foundInScope) + *foundInScope = 0; return _engine->undefinedValue(); } diff --git a/src/libs/qmljs/qmljsinterpreter.h b/src/libs/qmljs/qmljsinterpreter.h index f872a7a2238..4474dd43002 100644 --- a/src/libs/qmljs/qmljsinterpreter.h +++ b/src/libs/qmljs/qmljsinterpreter.h @@ -285,7 +285,7 @@ public: const TypeEnvironment *typeEnvironment(const Document *doc) const; void setTypeEnvironment(const Document *doc, const TypeEnvironment *typeEnvironment); - const Value *lookup(const QString &name) const; + const Value *lookup(const QString &name, const ObjectValue **foundInScope = 0) const; const ObjectValue *lookupType(const Document *doc, AST::UiQualifiedId *qmlTypeName) const; const ObjectValue *lookupType(const Document *doc, const QStringList &qmlTypeName) const; const Value *lookupReference(const Reference *reference) const; diff --git a/src/libs/qmljs/qmljslink.cpp b/src/libs/qmljs/qmljslink.cpp index 9539cfc581c..55e7a425224 100644 --- a/src/libs/qmljs/qmljslink.cpp +++ b/src/libs/qmljs/qmljslink.cpp @@ -114,7 +114,6 @@ Link::Link(Context *context, const Document::Ptr &doc, const Snapshot &snapshot, d->importPaths = importPaths; linkImports(); - initializeScopeChain(); } Link::~Link() @@ -133,86 +132,6 @@ QList<DiagnosticMessage> Link::diagnosticMessages() const return d->diagnosticMessages; } -void Link::initializeScopeChain() -{ - Q_D(Link); - - ScopeChain &scopeChain = d->context->scopeChain(); - - // ### TODO: This object ought to contain the global namespace additions by QML. - scopeChain.globalScope = engine()->globalObject(); - - if (! d->doc) { - scopeChain.update(); - return; - } - - Bind *bind = d->doc->bind(); - QHash<Document *, ScopeChain::QmlComponentChain *> componentScopes; - - ScopeChain::QmlComponentChain *chain = new ScopeChain::QmlComponentChain; - scopeChain.qmlComponentScope = QSharedPointer<const ScopeChain::QmlComponentChain>(chain); - if (d->doc->qmlProgram()) { - componentScopes.insert(d->doc.data(), chain); - makeComponentChain(d->doc, chain, &componentScopes); - - if (const TypeEnvironment *typeEnvironment = d->context->typeEnvironment(d->doc.data())) - scopeChain.qmlTypes = typeEnvironment; - } else { - // add scope chains for all components that import this file - foreach (Document::Ptr otherDoc, d->snapshot) { - foreach (const ImportInfo &import, otherDoc->bind()->imports()) { - if (import.type() == ImportInfo::FileImport && d->doc->fileName() == import.name()) { - ScopeChain::QmlComponentChain *component = new ScopeChain::QmlComponentChain; - componentScopes.insert(otherDoc.data(), component); - chain->instantiatingComponents += component; - makeComponentChain(otherDoc, component, &componentScopes); - } - } - } - - // ### TODO: Which type environment do scripts see? - - if (bind->rootObjectValue()) - scopeChain.jsScopes += bind->rootObjectValue(); - } - - scopeChain.update(); -} - -void Link::makeComponentChain( - Document::Ptr doc, - ScopeChain::QmlComponentChain *target, - QHash<Document *, ScopeChain::QmlComponentChain *> *components) -{ - Q_D(Link); - - if (!doc->qmlProgram()) - return; - - Bind *bind = doc->bind(); - - // add scopes for all components instantiating this one - foreach (Document::Ptr otherDoc, d->snapshot) { - if (otherDoc == doc) - continue; - if (otherDoc->bind()->usesQmlPrototype(bind->rootObjectValue(), d->context)) { - if (components->contains(otherDoc.data())) { -// target->instantiatingComponents += components->value(otherDoc.data()); - } else { - ScopeChain::QmlComponentChain *component = new ScopeChain::QmlComponentChain; - components->insert(otherDoc.data(), component); - target->instantiatingComponents += component; - - makeComponentChain(otherDoc, component, components); - } - } - } - - // build this component scope - target->document = doc; -} - void Link::linkImports() { Q_D(Link); diff --git a/src/libs/qmljs/qmljslink.h b/src/libs/qmljs/qmljslink.h index d81fcc509a6..3a922805549 100644 --- a/src/libs/qmljs/qmljslink.h +++ b/src/libs/qmljs/qmljslink.h @@ -63,17 +63,9 @@ public: private: Interpreter::Engine *engine(); - void makeComponentChain( - Document::Ptr doc, - Interpreter::ScopeChain::QmlComponentChain *target, - QHash<Document *, Interpreter::ScopeChain::QmlComponentChain *> *components); - - static QList<Document::Ptr> reachableDocuments(Document::Ptr startDoc, const Snapshot &snapshot, - const QStringList &importPaths); static AST::UiQualifiedId *qualifiedTypeNameId(AST::Node *node); void linkImports(); - void initializeScopeChain(); void populateImportedTypes(Interpreter::TypeEnvironment *typeEnv, Document::Ptr doc); Interpreter::ObjectValue *importFile(Document::Ptr doc, const Interpreter::ImportInfo &importInfo); diff --git a/src/libs/qmljs/qmljslookupcontext.cpp b/src/libs/qmljs/qmljslookupcontext.cpp index d9439481076..5b244b7faec 100644 --- a/src/libs/qmljs/qmljslookupcontext.cpp +++ b/src/libs/qmljs/qmljslookupcontext.cpp @@ -46,7 +46,7 @@ public: // since we keep the document and snapshot around, we don't need to keep the Link instance Link link(&context, doc, snapshot, ModelManagerInterface::instance()->importPaths()); - ScopeBuilder scopeBuilder(doc, &context); + ScopeBuilder scopeBuilder(&context, doc, snapshot); scopeBuilder.push(path); } @@ -57,7 +57,7 @@ public: doc(doc), snapshot(snapshot) { - ScopeBuilder scopeBuilder(doc, &context); + ScopeBuilder scopeBuilder(&context, doc, snapshot); scopeBuilder.push(path); } diff --git a/src/libs/qmljs/qmljsmodelmanagerinterface.h b/src/libs/qmljs/qmljsmodelmanagerinterface.h index faec7f58da6..d833fbb7491 100644 --- a/src/libs/qmljs/qmljsmodelmanagerinterface.h +++ b/src/libs/qmljs/qmljsmodelmanagerinterface.h @@ -80,13 +80,39 @@ public: QStringList importPaths; }; + class WorkingCopy + { + public: + typedef QHash<QString, QPair<QString, int> > Table; + + void insert(const QString &fileName, const QString &source, int revision = 0) + { _elements.insert(fileName, qMakePair(source, revision)); } + + bool contains(const QString &fileName) const + { return _elements.contains(fileName); } + + QString source(const QString &fileName) const + { return _elements.value(fileName).first; } + + QPair<QString, int> get(const QString &fileName) const + { return _elements.value(fileName); } + + Table all() const + { return _elements; } + + private: + Table _elements; + }; + public: ModelManagerInterface(QObject *parent = 0); virtual ~ModelManagerInterface(); static ModelManagerInterface *instance(); + virtual WorkingCopy workingCopy() const = 0; virtual QmlJS::Snapshot snapshot() const = 0; + virtual void updateSourceFiles(const QStringList &files, bool emitDocumentOnDiskChanged) = 0; virtual void fileChangedOnDisk(const QString &path) = 0; diff --git a/src/libs/qmljs/qmljsscopebuilder.cpp b/src/libs/qmljs/qmljsscopebuilder.cpp index ceffa41b4b4..d1c9374b1d3 100644 --- a/src/libs/qmljs/qmljsscopebuilder.cpp +++ b/src/libs/qmljs/qmljsscopebuilder.cpp @@ -38,10 +38,12 @@ using namespace QmlJS; using namespace QmlJS::Interpreter; using namespace QmlJS::AST; -ScopeBuilder::ScopeBuilder(Document::Ptr doc, Interpreter::Context *context) +ScopeBuilder::ScopeBuilder(Context *context, Document::Ptr doc, const Snapshot &snapshot) : _doc(doc) + , _snapshot(snapshot) , _context(context) { + initializeScopeChain(); } ScopeBuilder::~ScopeBuilder() @@ -92,6 +94,85 @@ void ScopeBuilder::pop() _context->scopeChain().update(); } +void ScopeBuilder::initializeScopeChain() +{ + ScopeChain &scopeChain = _context->scopeChain(); + scopeChain = ScopeChain(); // reset + + Interpreter::Engine *engine = _context->engine(); + + // ### TODO: This object ought to contain the global namespace additions by QML. + scopeChain.globalScope = engine->globalObject(); + + if (! _doc) { + scopeChain.update(); + return; + } + + Bind *bind = _doc->bind(); + QHash<Document *, ScopeChain::QmlComponentChain *> componentScopes; + + ScopeChain::QmlComponentChain *chain = new ScopeChain::QmlComponentChain; + scopeChain.qmlComponentScope = QSharedPointer<const ScopeChain::QmlComponentChain>(chain); + if (_doc->qmlProgram()) { + componentScopes.insert(_doc.data(), chain); + makeComponentChain(_doc, chain, &componentScopes); + + if (const TypeEnvironment *typeEnvironment = _context->typeEnvironment(_doc.data())) + scopeChain.qmlTypes = typeEnvironment; + } else { + // add scope chains for all components that import this file + foreach (Document::Ptr otherDoc, _snapshot) { + foreach (const ImportInfo &import, otherDoc->bind()->imports()) { + if (import.type() == ImportInfo::FileImport && _doc->fileName() == import.name()) { + ScopeChain::QmlComponentChain *component = new ScopeChain::QmlComponentChain; + componentScopes.insert(otherDoc.data(), component); + chain->instantiatingComponents += component; + makeComponentChain(otherDoc, component, &componentScopes); + } + } + } + + // ### TODO: Which type environment do scripts see? + + if (bind->rootObjectValue()) + scopeChain.jsScopes += bind->rootObjectValue(); + } + + scopeChain.update(); +} + +void ScopeBuilder::makeComponentChain( + Document::Ptr doc, + ScopeChain::QmlComponentChain *target, + QHash<Document *, ScopeChain::QmlComponentChain *> *components) +{ + if (!doc->qmlProgram()) + return; + + Bind *bind = doc->bind(); + + // add scopes for all components instantiating this one + foreach (Document::Ptr otherDoc, _snapshot) { + if (otherDoc == doc) + continue; + if (otherDoc->bind()->usesQmlPrototype(bind->rootObjectValue(), _context)) { + if (components->contains(otherDoc.data())) { +// target->instantiatingComponents += components->value(otherDoc.data()); + } else { + ScopeChain::QmlComponentChain *component = new ScopeChain::QmlComponentChain; + components->insert(otherDoc.data(), component); + target->instantiatingComponents += component; + + makeComponentChain(otherDoc, component, components); + } + } + } + + // build this component scope + target->document = doc; +} + void ScopeBuilder::setQmlScopeObject(Node *node) { ScopeChain &scopeChain = _context->scopeChain(); diff --git a/src/libs/qmljs/qmljsscopebuilder.h b/src/libs/qmljs/qmljsscopebuilder.h index 5c33a27393b..3fc44052a14 100644 --- a/src/libs/qmljs/qmljsscopebuilder.h +++ b/src/libs/qmljs/qmljsscopebuilder.h @@ -31,6 +31,7 @@ #define QMLJSSCOPEBUILDER_H #include <qmljs/qmljsdocument.h> +#include <qmljs/qmljsinterpreter.h> #include <QtCore/QList> @@ -40,16 +41,10 @@ namespace AST { class Node; } -namespace Interpreter { - class Context; - class Value; - class ObjectValue; -} - class QMLJS_EXPORT ScopeBuilder { public: - ScopeBuilder(Document::Ptr doc, Interpreter::Context *context); + ScopeBuilder(Interpreter::Context *context, Document::Ptr doc, const Snapshot &snapshot); ~ScopeBuilder(); void push(AST::Node *node); @@ -59,10 +54,15 @@ public: static const Interpreter::ObjectValue *isPropertyChangesObject(const Interpreter::Context *context, const Interpreter::ObjectValue *object); private: + void initializeScopeChain(); + void makeComponentChain(Document::Ptr doc, Interpreter::ScopeChain::QmlComponentChain *target, + QHash<Document *, Interpreter::ScopeChain::QmlComponentChain *> *components); + void setQmlScopeObject(AST::Node *node); const Interpreter::Value *scopeObjectLookup(AST::UiQualifiedId *id); Document::Ptr _doc; + Snapshot _snapshot; Interpreter::Context *_context; QList<AST::Node *> _nodes; }; diff --git a/src/plugins/qmldesigner/designercore/model/texttomodelmerger.cpp b/src/plugins/qmldesigner/designercore/model/texttomodelmerger.cpp index 49c990d04b6..0627e0e6e88 100644 --- a/src/plugins/qmldesigner/designercore/model/texttomodelmerger.cpp +++ b/src/plugins/qmldesigner/designercore/model/texttomodelmerger.cpp @@ -205,7 +205,7 @@ public: , m_doc(doc) , m_context(new Interpreter::Context) , m_link(m_context, doc, snapshot, importPaths) - , m_scopeBuilder(doc, m_context) + , m_scopeBuilder(m_context, doc, snapshot) { } diff --git a/src/plugins/qmljseditor/qmljseditor.cpp b/src/plugins/qmljseditor/qmljseditor.cpp index de0f29823b4..c983f50ec8d 100644 --- a/src/plugins/qmljseditor/qmljseditor.cpp +++ b/src/plugins/qmljseditor/qmljseditor.cpp @@ -35,6 +35,7 @@ #include "qmljseditorcodeformatter.h" #include "qmljsquickfix.h" #include "qmloutlinemodel.h" +#include "qmljsfindreferences.h" #include <qmljs/qmljsbind.h> #include <qmljs/qmljscheck.h> @@ -676,7 +677,8 @@ QmlJSTextEditor::QmlJSTextEditor(QWidget *parent) : m_outlineModel(new QmlOutlineModel(this)), m_modelManager(0), m_contextPane(0), - m_updateSelectedElements(false) + m_updateSelectedElements(false), + m_findReferences(new FindReferences(this)) { qRegisterMetaType<QmlJSEditor::Internal::SemanticInfo>("QmlJSEditor::Internal::SemanticInfo"); @@ -1434,6 +1436,11 @@ void QmlJSTextEditor::followSymbolUnderCursor() openLink(findLinkAt(textCursor())); } +void QmlJSTextEditor::findUsages() +{ + m_findReferences->findUsages(file()->fileName(), textCursor().position()); +} + void QmlJSTextEditor::showContextPane() { if (m_contextPane) { diff --git a/src/plugins/qmljseditor/qmljseditor.h b/src/plugins/qmljseditor/qmljseditor.h index 1d7d57b82c9..8071dd03d80 100644 --- a/src/plugins/qmljseditor/qmljseditor.h +++ b/src/plugins/qmljseditor/qmljseditor.h @@ -63,6 +63,7 @@ namespace QmlJS { */ namespace QmlJSEditor { class Highlighter; +class FindReferences; namespace Internal { @@ -244,6 +245,7 @@ public: public slots: void followSymbolUnderCursor(); + void findUsages(); void showContextPane(); virtual void setFontSettings(const TextEditor::FontSettings &); @@ -331,6 +333,8 @@ private: QmlJS::IContextPane *m_contextPane; int m_oldCursorPosition; bool m_updateSelectedElements; + + FindReferences *m_findReferences; }; } // namespace Internal diff --git a/src/plugins/qmljseditor/qmljseditor.pro b/src/plugins/qmljseditor/qmljseditor.pro index 976a8292819..8a6c725379c 100644 --- a/src/plugins/qmljseditor/qmljseditor.pro +++ b/src/plugins/qmljseditor/qmljseditor.pro @@ -31,7 +31,8 @@ HEADERS += \ qmljsoutlinetreeview.h \ quicktoolbarsettingspage.h \ quicktoolbar.h \ - qmljscomponentnamedialog.h + qmljscomponentnamedialog.h \ + qmljsfindreferences.h SOURCES += \ qmljscodecompletion.cpp \ @@ -56,7 +57,8 @@ SOURCES += \ qmljsoutlinetreeview.cpp \ quicktoolbarsettingspage.cpp \ quicktoolbar.cpp \ - qmljscomponentnamedialog.cpp + qmljscomponentnamedialog.cpp \ + qmljsfindreferences.cpp RESOURCES += qmljseditor.qrc OTHER_FILES += QmlJSEditor.pluginspec QmlJSEditor.mimetypes.xml diff --git a/src/plugins/qmljseditor/qmljseditorconstants.h b/src/plugins/qmljseditor/qmljseditorconstants.h index e6f19b6112b..342f6e94acf 100644 --- a/src/plugins/qmljseditor/qmljseditorconstants.h +++ b/src/plugins/qmljseditor/qmljseditorconstants.h @@ -47,8 +47,10 @@ const char * const RUN_SEP = "QmlJSEditor.Run.Separator"; const char * const C_QMLJSEDITOR_ID = "QMLProjectManager.QMLJSEditor"; const char * const C_QMLJSEDITOR_DISPLAY_NAME = QT_TRANSLATE_NOOP("OpenWith::Editors", "QMLJS Editor"); const char * const TASK_INDEX = "QmlJSEditor.TaskIndex"; +const char * const TASK_SEARCH = "QmlJSEditor.TaskSearch"; const char * const FOLLOW_SYMBOL_UNDER_CURSOR = "QmlJSEditor.FollowSymbolUnderCursor"; +const char * const FIND_USAGES = "QmlJSEditor.FindUsages"; const char * const SHOW_QT_QUICK_HELPER = "QmlJSEditor.ShowQtQuickHelper"; const char * const QML_MIMETYPE = "application/x-qml"; diff --git a/src/plugins/qmljseditor/qmljseditorplugin.cpp b/src/plugins/qmljseditor/qmljseditorplugin.cpp index 160d047cca1..e7cbfde8131 100644 --- a/src/plugins/qmljseditor/qmljseditorplugin.cpp +++ b/src/plugins/qmljseditor/qmljseditorplugin.cpp @@ -164,6 +164,13 @@ bool QmlJSEditorPlugin::initialize(const QStringList & /*arguments*/, QString *e contextMenu->addAction(cmd); qmlToolsMenu->addAction(cmd); + QAction *findUsagesAction = new QAction(tr("Find Usages"), this); + cmd = am->registerAction(findUsagesAction, Constants::FIND_USAGES, context); + cmd->setDefaultKeySequence(QKeySequence(tr("Ctrl+Shift+U"))); + connect(findUsagesAction, SIGNAL(triggered()), this, SLOT(findUsages())); + contextMenu->addAction(cmd); + qmlToolsMenu->addAction(cmd); + QAction *showQuickToolbar = new QAction(tr("Show Qt Quick Toolbar"), this); cmd = am->registerAction(showQuickToolbar, Constants::SHOW_QT_QUICK_HELPER, context); cmd->setDefaultKeySequence(QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_Space)); @@ -261,6 +268,13 @@ void QmlJSEditorPlugin::followSymbolUnderCursor() editor->followSymbolUnderCursor(); } +void QmlJSEditorPlugin::findUsages() +{ + Core::EditorManager *em = Core::EditorManager::instance(); + if (QmlJSTextEditor *editor = qobject_cast<QmlJSTextEditor*>(em->currentEditor()->widget())) + editor->findUsages(); +} + void QmlJSEditorPlugin::showContextPane() { Core::EditorManager *em = Core::EditorManager::instance(); diff --git a/src/plugins/qmljseditor/qmljseditorplugin.h b/src/plugins/qmljseditor/qmljseditorplugin.h index 96702df75b8..b45c867dbc7 100644 --- a/src/plugins/qmljseditor/qmljseditorplugin.h +++ b/src/plugins/qmljseditor/qmljseditorplugin.h @@ -89,6 +89,7 @@ public: public Q_SLOTS: void followSymbolUnderCursor(); + void findUsages(); void showContextPane(); private Q_SLOTS: diff --git a/src/plugins/qmljseditor/qmljsfindreferences.cpp b/src/plugins/qmljseditor/qmljsfindreferences.cpp new file mode 100644 index 00000000000..d3f9de96a00 --- /dev/null +++ b/src/plugins/qmljseditor/qmljsfindreferences.cpp @@ -0,0 +1,685 @@ +/************************************************************************** +** +** This file is part of Qt Creator +** +** Copyright (c) 2010 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 "qmljsfindreferences.h" + +#include <texteditor/basetexteditor.h> +#include <texteditor/basefilefind.h> +#include <find/searchresultwindow.h> +#include <extensionsystem/pluginmanager.h> +#include <utils/filesearch.h> +#include <coreplugin/progressmanager/progressmanager.h> +#include <coreplugin/progressmanager/futureprogress.h> +#include <coreplugin/editormanager/editormanager.h> +#include <coreplugin/icore.h> + +#include <qmljs/qmljsmodelmanagerinterface.h> +#include <qmljs/qmljsbind.h> +#include <qmljs/qmljslink.h> +#include <qmljs/qmljsevaluate.h> +#include <qmljs/qmljsscopebuilder.h> +#include <qmljs/parser/qmljsastvisitor_p.h> +#include <qmljs/parser/qmljsast_p.h> + +#include "qmljseditorconstants.h" + +#include <QtCore/QTime> +#include <QtCore/QTimer> +#include <QtCore/QtConcurrentRun> +#include <QtCore/QtConcurrentMap> +#include <QtCore/QDir> +#include <QtGui/QApplication> +#include <qtconcurrent/runextensions.h> + +#include <functional> + +using namespace QmlJS; +using namespace QmlJS::Interpreter; +using namespace QmlJS::AST; +using namespace QmlJSEditor; + +static const ObjectValue *prototypeWithMember(const Context *context, const ObjectValue *object, const QString &name) +{ + if (!object) + return 0; + const Value *value = object->property(name, context); + if (!value) + return 0; + forever { + const ObjectValue *prototype = object->prototype(context); + if (!prototype || prototype->property(name, context) != value) + return object; + object = prototype; + } +} + +namespace { + +// ### These visitors could be useful in general + +class FindUsages: protected Visitor +{ +public: + typedef QList<AST::SourceLocation> Result; + + FindUsages(Document::Ptr doc, const Snapshot &snapshot, Context *context) + : _doc(doc) + , _snapshot(snapshot) + , _context(context) + , _builder(context, doc, snapshot) + { + } + + Result operator()(const QString &name, const ObjectValue *scope) + { + _name = name; + _scope = scope; + _usages.clear(); + if (_doc) + Node::accept(_doc->ast(), this); + return _usages; + } + +protected: + void accept(AST::Node *node) + { AST::Node::acceptChild(node, this); } + + using Visitor::visit; + + virtual bool visit(AST::UiPublicMember *node) + { + if (node->name + && node->name->asString() == _name + && _context->scopeChain().qmlScopeObjects.contains(_scope)) { + _usages.append(node->identifierToken); + } + + return true; + } + + virtual bool visit(AST::UiObjectDefinition *node) + { + _builder.push(node); + Node::accept(node->initializer, this); + _builder.pop(); + return false; + } + + virtual bool visit(AST::UiObjectBinding *node) + { + if (node->qualifiedId + && !node->qualifiedId->next + && node->qualifiedId->name->asString() == _name + && checkQmlScope()) { + _usages.append(node->qualifiedId->identifierToken); + } + + _builder.push(node); + Node::accept(node->initializer, this); + _builder.pop(); + return false; + } + + virtual bool visit(AST::UiScriptBinding *node) + { + if (node->qualifiedId + && !node->qualifiedId->next + && node->qualifiedId->name->asString() == _name + && checkQmlScope()) { + _usages.append(node->qualifiedId->identifierToken); + } + return true; + } + + virtual bool visit(AST::UiArrayBinding *node) + { + if (node->qualifiedId + && !node->qualifiedId->next + && node->qualifiedId->name->asString() == _name + && checkQmlScope()) { + _usages.append(node->qualifiedId->identifierToken); + } + return true; + } + + virtual bool visit(AST::IdentifierExpression *node) + { + if (!node->name || node->name->asString() != _name) + return false; + + const ObjectValue *scope; + _context->lookup(_name, &scope); + if (!scope) + return false; + if (check(scope)) { + _usages.append(node->identifierToken); + return false; + } + + // the order of scopes in 'instantiatingComponents' is undefined, + // so it might still be a use - we just found a different value in a different scope first + + // if scope is one of these, our match wasn't inside the instantiating components list + const ScopeChain &chain = _context->scopeChain(); + if (chain.jsScopes.contains(scope) + || chain.qmlScopeObjects.contains(scope) + || chain.qmlTypes == scope + || chain.globalScope == scope) + return false; + + if (contains(chain.qmlComponentScope.data())) + _usages.append(node->identifierToken); + + return false; + } + + virtual bool visit(AST::FieldMemberExpression *node) + { + if (!node->name || node->name->asString() != _name) + return true; + + Evaluate evaluate(_context); + const Value *lhsValue = evaluate(node->base); + if (!lhsValue) + return true; + + if (check(lhsValue->asObjectValue())) // passing null is ok + _usages.append(node->identifierToken); + + return true; + } + + virtual bool visit(AST::FunctionDeclaration *node) + { + if (node->name && node->name->asString() == _name) { + if (checkLookup()) + _usages.append(node->identifierToken); + } + Node::accept(node->formals, this); + _builder.push(node); + Node::accept(node->body, this); + _builder.pop(); + return false; + } + + virtual bool visit(AST::VariableDeclaration *node) + { + if (node->name && node->name->asString() == _name) { + if (checkLookup()) + _usages.append(node->identifierToken); + } + return true; + } + +private: + bool contains(const ScopeChain::QmlComponentChain *chain) + { + if (!chain || !chain->document) + return false; + + if (chain->document->bind()->idEnvironment()->property(_name, _context)) + return chain->document->bind()->idEnvironment() == _scope; + const ObjectValue *root = chain->document->bind()->rootObjectValue(); + if (root->property(_name, _context)) { + return check(root); + } + + foreach (const ScopeChain::QmlComponentChain *parent, chain->instantiatingComponents) { + if (contains(parent)) + return true; + } + return false; + } + + bool check(const ObjectValue *s) + { + if (!s) + return false; + return prototypeWithMember(_context, s, _name) == _scope; + } + + bool checkQmlScope() + { + foreach (const ObjectValue *s, _context->scopeChain().qmlScopeObjects) { + if (check(s)) + return true; + } + return false; + } + + bool checkLookup() + { + const ObjectValue *scope = 0; + _context->lookup(_name, &scope); + return check(scope); + } + + Result _usages; + + Document::Ptr _doc; + Snapshot _snapshot; + Context *_context; + ScopeBuilder _builder; + + QString _name; + const ObjectValue *_scope; +}; + +class ScopeAstPath: protected Visitor +{ +public: + ScopeAstPath(Document::Ptr doc) + : _doc(doc) + { + } + + QList<Node *> operator()(quint32 offset) + { + _result.clear(); + _offset = offset; + if (_doc) + Node::accept(_doc->ast(), this); + return _result; + } + +protected: + void accept(AST::Node *node) + { AST::Node::acceptChild(node, this); } + + using Visitor::visit; + + virtual bool preVisit(Node *node) + { + if (Statement *stmt = node->statementCast()) { + return containsOffset(stmt->firstSourceLocation(), stmt->lastSourceLocation()); + } else if (ExpressionNode *exp = node->expressionCast()) { + return containsOffset(exp->firstSourceLocation(), exp->lastSourceLocation()); + } else if (UiObjectMember *ui = node->uiObjectMemberCast()) { + return containsOffset(ui->firstSourceLocation(), ui->lastSourceLocation()); + } + return true; + } + + virtual bool visit(AST::UiObjectDefinition *node) + { + _result.append(node); + Node::accept(node->initializer, this); + return false; + } + + virtual bool visit(AST::UiObjectBinding *node) + { + _result.append(node); + Node::accept(node->initializer, this); + return false; + } + + virtual bool visit(AST::FunctionDeclaration *node) + { + Node::accept(node->formals, this); + _result.append(node); + Node::accept(node->body, this); + return false; + } + +private: + bool containsOffset(SourceLocation start, SourceLocation end) + { + return _offset >= start.begin() && _offset <= end.end(); + } + + QList<Node *> _result; + Document::Ptr _doc; + quint32 _offset; +}; + +class FindTargetExpression: protected Visitor +{ +public: + FindTargetExpression(Document::Ptr doc) + : _doc(doc) + { + } + + QPair<Node *, QString> operator()(quint32 offset) + { + _result = qMakePair((Node *)0, QString()); + _offset = offset; + if (_doc) + Node::accept(_doc->ast(), this); + return _result; + } + +protected: + void accept(AST::Node *node) + { AST::Node::acceptChild(node, this); } + + using Visitor::visit; + + virtual bool preVisit(Node *node) + { + if (Statement *stmt = node->statementCast()) { + return containsOffset(stmt->firstSourceLocation(), stmt->lastSourceLocation()); + } else if (ExpressionNode *exp = node->expressionCast()) { + return containsOffset(exp->firstSourceLocation(), exp->lastSourceLocation()); + } else if (UiObjectMember *ui = node->uiObjectMemberCast()) { + return containsOffset(ui->firstSourceLocation(), ui->lastSourceLocation()); + } + return true; + } + + virtual bool visit(IdentifierExpression *node) + { + if (containsOffset(node->identifierToken)) + _result.second = node->name->asString(); + return true; + } + + virtual bool visit(FieldMemberExpression *node) + { + if (containsOffset(node->identifierToken)) { + _result.first = node->base; + _result.second = node->name->asString(); + return false; + } + return true; + } + + virtual bool visit(UiScriptBinding *node) + { + return !checkBindingName(node->qualifiedId); + } + + virtual bool visit(UiArrayBinding *node) + { + return !checkBindingName(node->qualifiedId); + } + + virtual bool visit(UiObjectBinding *node) + { + return !checkBindingName(node->qualifiedId); + } + + virtual bool visit(UiPublicMember *node) + { + if (containsOffset(node->identifierToken)) { + _result.second = node->name->asString(); + return false; + } + return true; + } + + // ### Misses function declaration, var declaration + + virtual bool visit(FunctionDeclaration *node) + { + if (containsOffset(node->identifierToken)) { + _result.second = node->name->asString(); + return false; + } + return true; + } + + virtual bool visit(VariableDeclaration *node) + { + if (containsOffset(node->identifierToken)) { + _result.second = node->name->asString(); + return false; + } + return true; + } + +private: + bool containsOffset(SourceLocation start, SourceLocation end) + { + return _offset >= start.begin() && _offset <= end.end(); + } + + bool containsOffset(SourceLocation loc) + { + return _offset >= loc.begin() && _offset <= loc.end(); + } + + bool checkBindingName(UiQualifiedId *id) + { + if (id && !id->next && containsOffset(id->identifierToken)) { + _result.second = id->name->asString(); + return true; + } + return false; + } + + QPair<Node *, QString> _result; + Document::Ptr _doc; + quint32 _offset; +}; + +class ProcessFile: public std::unary_function<QString, QList<FindReferences::Usage> > +{ + const Snapshot &snapshot; + const Context &context; + typedef FindReferences::Usage Usage; + QString name; + const ObjectValue *scope; + +public: + ProcessFile(const Snapshot &snapshot, + const Context &context, + QString name, + const ObjectValue *scope) + : snapshot(snapshot), context(context), name(name), scope(scope) + { } + + QList<Usage> operator()(const QString &fileName) + { + QList<Usage> usages; + + Document::Ptr doc = snapshot.document(fileName); + if (!doc) + return usages; + + Context contextCopy(context); + + // find all idenfifier expressions, try to resolve them and check if the result is in scope + FindUsages findUsages(doc, snapshot, &contextCopy); + FindUsages::Result results = findUsages(name, scope); + foreach (AST::SourceLocation loc, results) + usages.append(Usage(fileName, matchingLine(loc.offset, doc->source()), loc.startLine, loc.startColumn - 1, loc.length)); + + return usages; + } + + static QString matchingLine(unsigned position, const QString &source) + { + int start = source.lastIndexOf(QLatin1Char('\n'), position); + start += 1; + int end = source.indexOf(QLatin1Char('\n'), position); + + return source.mid(start, end - start); + } +}; + +class UpdateUI: public std::binary_function<QList<FindReferences::Usage> &, QList<FindReferences::Usage>, void> +{ + typedef FindReferences::Usage Usage; + QFutureInterface<Usage> *future; + +public: + UpdateUI(QFutureInterface<Usage> *future): future(future) {} + + void operator()(QList<Usage> &, const QList<Usage> &usages) + { + foreach (const Usage &u, usages) + future->reportResult(u); + + future->setProgressValue(future->progressValue() + 1); + } +}; + +} // end of anonymous namespace + +FindReferences::FindReferences(QObject *parent) + : QObject(parent) + , _resultWindow(Find::SearchResultWindow::instance()) +{ + m_watcher.setPendingResultsLimit(1); + connect(&m_watcher, SIGNAL(resultsReadyAt(int,int)), this, SLOT(displayResults(int,int))); + connect(&m_watcher, SIGNAL(finished()), this, SLOT(searchFinished())); +} + +FindReferences::~FindReferences() +{ +} + +static void find_helper(QFutureInterface<FindReferences::Usage> &future, + const ModelManagerInterface::WorkingCopy workingCopy, + Snapshot snapshot, + const QString fileName, + quint32 offset) +{ + // update snapshot from workingCopy to make sure it's up to date + // ### remove? + // ### this is a great candidate for map-reduce + QHashIterator< QString, QPair<QString, int> > it(workingCopy.all()); + while (it.hasNext()) { + it.next(); + Document::Ptr oldDoc = snapshot.document(it.key()); + if (oldDoc && oldDoc->editorRevision() == it.value().second) + continue; + + Document::Ptr newDoc = snapshot.documentFromSource(it.key(), it.value().first); + newDoc->parse(); + snapshot.insert(newDoc); + } + + // find the scope for the name we're searching + Context context; + Document::Ptr doc = snapshot.document(fileName); + if (!doc) + return; + + Link link(&context, doc, snapshot, ModelManagerInterface::instance()->importPaths()); + ScopeBuilder builder(&context, doc, snapshot); + ScopeAstPath astPath(doc); + builder.push(astPath(offset)); + + FindTargetExpression findTarget(doc); + QPair<Node *, QString> target = findTarget(offset); + const QString &name = target.second; + if (name.isEmpty()) + return; + + const ObjectValue *scope = 0; + if (target.first) { + Evaluate evaluate(&context); + const Value *v = evaluate(target.first); + if (v) + scope = v->asObjectValue(); + } else { + context.lookup(name, &scope); + } + if (!scope) + return; + scope = prototypeWithMember(&context, scope, name); + if (!scope) + return; + + QStringList files; + foreach (const Document::Ptr &doc, snapshot) { + // ### skip files that don't contain the name token + files.append(doc->fileName()); + } + + future.setProgressRange(0, files.size()); + + ProcessFile process(snapshot, context, name, scope); + UpdateUI reduce(&future); + + QtConcurrent::blockingMappedReduced<QList<FindReferences::Usage> > (files, process, reduce); + + future.setProgressValue(files.size()); +} + +void FindReferences::findUsages(const QString &fileName, quint32 offset) +{ + Find::SearchResult *search = _resultWindow->startNewSearch(Find::SearchResultWindow::SearchOnly); + + connect(search, SIGNAL(activated(Find::SearchResultItem)), + this, SLOT(openEditor(Find::SearchResultItem))); + + findAll_helper(fileName, offset); +} + +void FindReferences::findAll_helper(const QString &fileName, quint32 offset) +{ + _resultWindow->popup(true); + + ModelManagerInterface *modelManager = ModelManagerInterface::instance(); + + + QFuture<Usage> result = QtConcurrent::run( + &find_helper, modelManager->workingCopy(), + modelManager->snapshot(), fileName, offset); + m_watcher.setFuture(result); + + Core::ProgressManager *progressManager = Core::ICore::instance()->progressManager(); + Core::FutureProgress *progress = progressManager->addTask(result, tr("Searching"), + QmlJSEditor::Constants::TASK_SEARCH); + + connect(progress, SIGNAL(clicked()), _resultWindow, SLOT(popup())); +} + +void FindReferences::displayResults(int first, int last) +{ + for (int index = first; index != last; ++index) { + Usage result = m_watcher.future().resultAt(index); + _resultWindow->addResult(result.path, + result.line, + result.lineText, + result.col, + result.len); + } +} + +void FindReferences::searchFinished() +{ + _resultWindow->finishSearch(); + emit changed(); +} + +void FindReferences::openEditor(const Find::SearchResultItem &item) +{ + if (item.path.size() > 0) { + TextEditor::BaseTextEditor::openEditorAt(item.path.first(), item.lineNumber, item.textMarkPos, + QString(), + Core::EditorManager::ModeSwitch); + } else { + Core::EditorManager::instance()->openEditor(item.text, QString(), Core::EditorManager::ModeSwitch); + } +} diff --git a/src/plugins/qmljseditor/qmljsfindreferences.h b/src/plugins/qmljseditor/qmljsfindreferences.h new file mode 100644 index 00000000000..880cadb55a5 --- /dev/null +++ b/src/plugins/qmljseditor/qmljsfindreferences.h @@ -0,0 +1,97 @@ +/************************************************************************** +** +** This file is part of Qt Creator +** +** Copyright (c) 2010 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. +** +**************************************************************************/ + +#ifndef QMLJSFINDREFERENCES_H +#define QMLJSFINDREFERENCES_H + +#include <QtCore/QMutex> +#include <QtCore/QObject> +#include <QtCore/QPointer> +#include <QtCore/QFuture> +#include <QtCore/QFutureWatcher> +#include <utils/filesearch.h> +#include <qmljs/qmljsdocument.h> +#include <qmljs/qmljslookupcontext.h> + +QT_FORWARD_DECLARE_CLASS(QTimer) + +namespace Find { + class SearchResultWindow; + struct SearchResultItem; +} // end of namespace Find + +namespace QmlJSEditor { + +class FindReferences: public QObject +{ + Q_OBJECT +public: + class Usage + { + public: + Usage() + : line(0), col(0), len(0) {} + + Usage(const QString &path, const QString &lineText, int line, int col, int len) + : path(path), lineText(lineText), line(line), col(col), len(len) {} + + public: + QString path; + QString lineText; + int line; + int col; + int len; + }; + +public: + FindReferences(QObject *parent = 0); + virtual ~FindReferences(); + +Q_SIGNALS: + void changed(); + +public: + void findUsages(const QString &fileName, quint32 offset); + +private Q_SLOTS: + void displayResults(int first, int last); + void searchFinished(); + void openEditor(const Find::SearchResultItem &item); + +private: + void findAll_helper(const QString &fileName, quint32 offset); + +private: + Find::SearchResultWindow *_resultWindow; + QFutureWatcher<Usage> m_watcher; +}; + +} // end of namespace QmlJSEditor + +#endif // QMLJSFINDREFERENCES_H diff --git a/src/plugins/qmljseditor/qmljsmodelmanager.cpp b/src/plugins/qmljseditor/qmljsmodelmanager.cpp index 2dd2b256c2b..8a832bccfe3 100644 --- a/src/plugins/qmljseditor/qmljsmodelmanager.cpp +++ b/src/plugins/qmljseditor/qmljsmodelmanager.cpp @@ -103,6 +103,24 @@ void ModelManager::loadQmlTypeDescriptions(const QString &resourcePath) //loadQmlPluginTypes(QString()); } +ModelManagerInterface::WorkingCopy ModelManager::workingCopy() const +{ + WorkingCopy workingCopy; + Core::EditorManager *editorManager = m_core->editorManager(); + + foreach (Core::IEditor *editor, editorManager->openedEditors()) { + const QString key = editor->file()->fileName(); + + if (TextEditor::ITextEditor *textEditor = qobject_cast<TextEditor::ITextEditor*>(editor)) { + if (QmlJSTextEditor *ed = qobject_cast<QmlJSTextEditor *>(textEditor->widget())) { + workingCopy.insert(key, ed->toPlainText(), ed->document()->revision()); + } + } + } + + return workingCopy; +} + Snapshot ModelManager::snapshot() const { QMutexLocker locker(&m_mutex); @@ -123,10 +141,8 @@ QFuture<void> ModelManager::refreshSourceFiles(const QStringList &sourceFiles, return QFuture<void>(); } - const QMap<QString, WorkingCopy> workingCopy = buildWorkingCopyList(); - QFuture<void> result = QtConcurrent::run(&ModelManager::parse, - workingCopy, sourceFiles, + workingCopy(), sourceFiles, this, emitDocumentOnDiskChanged); @@ -151,29 +167,10 @@ QFuture<void> ModelManager::refreshSourceFiles(const QStringList &sourceFiles, return result; } -QMap<QString, ModelManager::WorkingCopy> ModelManager::buildWorkingCopyList() -{ - QMap<QString, WorkingCopy> workingCopy; - Core::EditorManager *editorManager = m_core->editorManager(); - - foreach (Core::IEditor *editor, editorManager->openedEditors()) { - const QString key = editor->file()->fileName(); - - if (TextEditor::ITextEditor *textEditor = qobject_cast<TextEditor::ITextEditor*>(editor)) { - if (QmlJSTextEditor *ed = qobject_cast<QmlJSTextEditor *>(textEditor->widget())) { - workingCopy[key].contents = ed->toPlainText(); - workingCopy[key].documentRevision = ed->document()->revision(); - } - } - } - - return workingCopy; -} - void ModelManager::fileChangedOnDisk(const QString &path) { QtConcurrent::run(&ModelManager::parse, - buildWorkingCopyList(), QStringList() << path, + workingCopy(), QStringList() << path, this, true); } @@ -340,7 +337,7 @@ static void findNewLibraryImports(const Document::Ptr &doc, const Snapshot &snap } void ModelManager::parse(QFutureInterface<void> &future, - QMap<QString, WorkingCopy> workingCopy, + WorkingCopy workingCopy, QStringList files, ModelManager *modelManager, bool emitDocChangedOnDisk) @@ -377,9 +374,9 @@ void ModelManager::parse(QFutureInterface<void> &future, int documentRevision = 0; if (workingCopy.contains(fileName)) { - WorkingCopy wc = workingCopy.value(fileName); - contents = wc.contents; - documentRevision = wc.documentRevision; + QPair<QString, int> entry = workingCopy.get(fileName); + contents = entry.first; + documentRevision = entry.second; } else { QFile inFile(fileName); diff --git a/src/plugins/qmljseditor/qmljsmodelmanager.h b/src/plugins/qmljseditor/qmljsmodelmanager.h index 54449674ccc..79e9937e4d7 100644 --- a/src/plugins/qmljseditor/qmljsmodelmanager.h +++ b/src/plugins/qmljseditor/qmljsmodelmanager.h @@ -53,7 +53,9 @@ class ModelManager: public QmlJS::ModelManagerInterface public: ModelManager(QObject *parent = 0); + virtual WorkingCopy workingCopy() const; virtual QmlJS::Snapshot snapshot() const; + virtual void updateSourceFiles(const QStringList &files, bool emitDocumentOnDiskChanged); virtual void fileChangedOnDisk(const QString &path); @@ -84,19 +86,11 @@ private Q_SLOTS: void qmlPluginTypeDumpError(QProcess::ProcessError error); protected: - struct WorkingCopy - { - WorkingCopy(int revision = 0): documentRevision(revision) {} - int documentRevision; - QString contents; - }; - QFuture<void> refreshSourceFiles(const QStringList &sourceFiles, bool emitDocumentOnDiskChanged); - QMap<QString, WorkingCopy> buildWorkingCopyList(); static void parse(QFutureInterface<void> &future, - QMap<QString, WorkingCopy> workingCopy, + WorkingCopy workingCopy, QStringList files, ModelManager *modelManager, bool emitDocChangedOnDisk); -- GitLab