qmljsmodelmanager.cpp 14.3 KB
Newer Older
1
2
3
4
/**************************************************************************
**
** This file is part of Qt Creator
**
hjk's avatar
hjk committed
5
** Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies).
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
**
** 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.
**
**************************************************************************/

30
#include "qmljseditorconstants.h"
Roberto Raggi's avatar
Roberto Raggi committed
31
#include "qmljsmodelmanager.h"
32
#include "qmljseditor.h"
33
34
35
36

#include <coreplugin/icore.h>
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/progressmanager/progressmanager.h>
37
#include <coreplugin/mimedatabase.h>
38
#include <qmljs/qmljsinterpreter.h>
39
#include <qmljs/qmljsbind.h>
40
#include <qmljs/parser/qmldirparser_p.h>
41
42
#include <texteditor/itexteditor.h>

43
#include <QDir>
44
45
#include <QFile>
#include <QFileInfo>
46
#include <QLibraryInfo>
47
48
49
#include <QtConcurrentRun>
#include <qtconcurrent/runextensions.h>
#include <QTextStream>
50

51
52
#include <QDebug>

53
using namespace QmlJS;
54
55
using namespace QmlJSEditor;
using namespace QmlJSEditor::Internal;
56

57
58
static QStringList environmentImportPaths();

Roberto Raggi's avatar
Roberto Raggi committed
59
60
ModelManager::ModelManager(QObject *parent):
        ModelManagerInterface(parent),
61
62
63
64
        m_core(Core::ICore::instance())
{
    m_synchronizer.setCancelOnWait(true);

65
    qRegisterMetaType<QmlJS::Document::Ptr>("QmlJS::Document::Ptr");
66
    qRegisterMetaType<QmlJS::LibraryInfo>("QmlJS::LibraryInfo");
67

68
69
    connect(this, SIGNAL(documentUpdated(QmlJS::Document::Ptr)),
            this, SLOT(onDocumentUpdated(QmlJS::Document::Ptr)));
70
71
    connect(this, SIGNAL(libraryInfoUpdated(QString,QmlJS::LibraryInfo)),
            this, SLOT(onLibraryInfoUpdated(QString,QmlJS::LibraryInfo)));
72
73

    loadQmlTypeDescriptions();
74
75
76

    m_defaultImportPaths << environmentImportPaths();
    m_defaultImportPaths << QLibraryInfo::location(QLibraryInfo::ImportsPath);
77
78
79
80
81
82
83
84
85
86
87
88
89
90
}

void ModelManager::loadQmlTypeDescriptions()
{
    const QString resourcePath = Core::ICore::instance()->resourcePath();
    const QDir typeFileDir(resourcePath + QLatin1String("/qml-type-descriptions"));
    const QStringList xmlExtensions = QStringList() << QLatin1String("*.xml");
    const QFileInfoList xmlFiles = typeFileDir.entryInfoList(xmlExtensions,
                                                             QDir::Files,
                                                             QDir::Name);

    const QStringList errors = Interpreter::MetaTypeSystem::load(xmlFiles);
    foreach (const QString &error, errors)
        qWarning() << qPrintable(error);
91
92
}

Roberto Raggi's avatar
Roberto Raggi committed
93
Snapshot ModelManager::snapshot() const
94
{
95
96
    QMutexLocker locker(&m_mutex);

97
98
99
    return _snapshot;
}

Roberto Raggi's avatar
Roberto Raggi committed
100
void ModelManager::updateSourceFiles(const QStringList &files)
101
{
102
    refreshSourceFiles(files);
103
104
}

Roberto Raggi's avatar
Roberto Raggi committed
105
QFuture<void> ModelManager::refreshSourceFiles(const QStringList &sourceFiles)
106
{
107
108
109
110
    if (sourceFiles.isEmpty()) {
        return QFuture<void>();
    }

111
    const QMap<QString, WorkingCopy> workingCopy = buildWorkingCopyList();
112

Roberto Raggi's avatar
Roberto Raggi committed
113
    QFuture<void> result = QtConcurrent::run(&ModelManager::parse,
114
115
                                              workingCopy, sourceFiles,
                                              this);
116

117
118
    if (m_synchronizer.futures().size() > 10) {
        QList<QFuture<void> > futures = m_synchronizer.futures();
119

120
        m_synchronizer.clearFutures();
121

122
123
124
        foreach (QFuture<void> future, futures) {
            if (! (future.isFinished() || future.isCanceled()))
                m_synchronizer.addFuture(future);
125
        }
126
    }
127

128
    m_synchronizer.addFuture(result);
129

130
131
    if (sourceFiles.count() > 1) {
        m_core->progressManager()->addTask(result, tr("Indexing"),
132
                        QmlJSEditor::Constants::TASK_INDEX);
133
    }
134
135

    return result;
136
137
}

Roberto Raggi's avatar
Roberto Raggi committed
138
QMap<QString, ModelManager::WorkingCopy> ModelManager::buildWorkingCopyList()
139
{
140
    QMap<QString, WorkingCopy> workingCopy;
141
142
143
144
145
146
    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)) {
147
148
149
150
            if (QmlJSTextEditor *ed = qobject_cast<QmlJSTextEditor *>(textEditor->widget())) {
                workingCopy[key].contents = ed->toPlainText();
                workingCopy[key].documentRevision = ed->document()->revision();
            }
151
152
153
154
155
156
        }
    }

    return workingCopy;
}

Roberto Raggi's avatar
Roberto Raggi committed
157
void ModelManager::emitDocumentUpdated(Document::Ptr doc)
158
159
{ emit documentUpdated(doc); }

Roberto Raggi's avatar
Roberto Raggi committed
160
void ModelManager::onDocumentUpdated(Document::Ptr doc)
161
{
162
163
    QMutexLocker locker(&m_mutex);

164
165
166
    _snapshot.insert(doc);
}

167
168
169
170
171
172
173
174
175
176
void ModelManager::emitLibraryInfoUpdated(const QString &path, const LibraryInfo &info)
{ emit libraryInfoUpdated(path, info); }

void ModelManager::onLibraryInfoUpdated(const QString &path, const LibraryInfo &info)
{
    QMutexLocker locker(&m_mutex);

    _snapshot.insertLibraryInfo(path, info);
}

177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
static QStringList qmlFilesInDirectory(const QString &path)
{
    // ### It would suffice to build pattern once. This function needs to be thread-safe.
    Core::MimeDatabase *db = Core::ICore::instance()->mimeDatabase();
    Core::MimeType jsSourceTy = db->findByType(QmlJSEditor::Constants::JS_MIMETYPE);
    Core::MimeType qmlSourceTy = db->findByType(QmlJSEditor::Constants::QML_MIMETYPE);

    QStringList pattern;
    foreach (const QRegExp &glob, jsSourceTy.globPatterns())
        pattern << glob.pattern();
    foreach (const QRegExp &glob, qmlSourceTy.globPatterns())
        pattern << glob.pattern();

    QStringList files;

    const QDir dir(path);
    foreach (const QFileInfo &fi, dir.entryInfoList(pattern, QDir::Files))
        files += fi.absoluteFilePath();

    return files;
}

static void findNewImplicitImports(const Document::Ptr &doc, const Snapshot &snapshot,
                            QStringList *importedFiles, QSet<QString> *scannedPaths)
{
    // scan files that could be implicitly imported
    // it's important we also do this for JS files, otherwise the isEmpty check will fail
    if (snapshot.documentsInDirectory(doc->path()).isEmpty()) {
        if (! scannedPaths->contains(doc->path())) {
            *importedFiles += qmlFilesInDirectory(doc->path());
            scannedPaths->insert(doc->path());
        }
    }
}

static void findNewFileImports(const Document::Ptr &doc, const Snapshot &snapshot,
                        QStringList *importedFiles, QSet<QString> *scannedPaths)
{
    // scan files and directories that are explicitly imported
    foreach (const QString &fileImport, doc->bind()->fileImports()) {
        const QFileInfo importFileInfo(doc->path() + QLatin1Char('/') + fileImport);
        const QString &importFilePath = importFileInfo.absoluteFilePath();
        if (importFileInfo.isFile()) {
            if (! snapshot.document(importFilePath))
                *importedFiles += importFilePath;
        } else if (importFileInfo.isDir()) {
            if (snapshot.documentsInDirectory(importFilePath).isEmpty()) {
                if (! scannedPaths->contains(importFilePath)) {
                    *importedFiles += qmlFilesInDirectory(importFilePath);
                    scannedPaths->insert(importFilePath);
                }
            }
        }
    }
}

static void findNewLibraryImports(const Document::Ptr &doc, const Snapshot &snapshot,
                           ModelManager *modelManager,
                           QStringList *importedFiles, QSet<QString> *scannedPaths)
{
    // scan library imports
238
239
    QStringList importPaths = modelManager->importPaths();
    importPaths.prepend(doc->path());
240
    foreach (const QString &libraryImport, doc->bind()->libraryImports()) {
241
        foreach (const QString &importPath, importPaths) {
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
            QDir dir(importPath);
            dir.cd(libraryImport);
            const QString targetPath = dir.absolutePath();

            // if we know there is a library, done
            if (snapshot.libraryInfo(targetPath).isValid())
                break;

            // if there is a qmldir file, we found a new library!
            if (dir.exists("qmldir")) {
                QFile qmldirFile(dir.filePath("qmldir"));
                qmldirFile.open(QFile::ReadOnly);
                QString qmldirData = QString::fromUtf8(qmldirFile.readAll());

                QmlDirParser qmldirParser;
                qmldirParser.setSource(qmldirData);
                qmldirParser.parse();

                modelManager->emitLibraryInfoUpdated(QFileInfo(qmldirFile).absolutePath(),
                                                     LibraryInfo(qmldirParser));

                // scan the qml files in the library
                foreach (const QmlDirParser::Component &component, qmldirParser.components()) {
                    if (! component.fileName.isEmpty()) {
                        QFileInfo componentFileInfo(dir.filePath(component.fileName));
                        const QString path = componentFileInfo.absolutePath();
                        if (! scannedPaths->contains(path)) {
                            *importedFiles += qmlFilesInDirectory(path);
                            scannedPaths->insert(path);
                        }
                    }
                }
            }
        }
    }
}

Roberto Raggi's avatar
Roberto Raggi committed
279
void ModelManager::parse(QFutureInterface<void> &future,
280
                            QMap<QString, WorkingCopy> workingCopy,
281
                            QStringList files,
Roberto Raggi's avatar
Roberto Raggi committed
282
                            ModelManager *modelManager)
283
{
284
285
286
287
    Core::MimeDatabase *db = Core::ICore::instance()->mimeDatabase();
    Core::MimeType jsSourceTy = db->findByType(QLatin1String("application/javascript"));
    Core::MimeType qmlSourceTy = db->findByType(QLatin1String("application/x-qml"));

288
289
290
291
292
293
294
    int progressRange = files.size();
    future.setProgressRange(0, progressRange);

    Snapshot snapshot = modelManager->_snapshot;

    // paths we have scanned for files and added to the files list
    QSet<QString> scannedPaths;
295
296

    for (int i = 0; i < files.size(); ++i) {
297
        future.setProgressValue(qreal(i) / files.size() * progressRange);
298
299

        const QString fileName = files.at(i);
300
301
302
303
304
305
306
307
308
309
310
311

        const QFileInfo fileInfo(fileName);
        Core::MimeType fileMimeTy = db->findByFile(fileInfo);

        bool isQmlFile = true;

        if (matchesMimeType(fileMimeTy, jsSourceTy))
            isQmlFile = false;

        else if (! matchesMimeType(fileMimeTy, qmlSourceTy))
            continue; // skip it. it's not a QML or a JS file.

312
        QString contents;
313
        int documentRevision = 0;
314
315

        if (workingCopy.contains(fileName)) {
316
317
318
            WorkingCopy wc = workingCopy.value(fileName);
            contents = wc.contents;
            documentRevision = wc.documentRevision;
319
320
321
322
323
324
325
326
327
328
        } else {
            QFile inFile(fileName);

            if (inFile.open(QIODevice::ReadOnly)) {
                QTextStream ins(&inFile);
                contents = ins.readAll();
                inFile.close();
            }
        }

329
330
        Document::Ptr doc = Document::create(fileName);
        doc->setDocumentRevision(documentRevision);
331
        doc->setSource(contents);
332
333
334
335
336
337
338
339
340
341
342
343
        doc->parse();

        // get list of referenced files not yet in snapshot or in directories already scanned
        QStringList importedFiles;
        findNewImplicitImports(doc, snapshot, &importedFiles, &scannedPaths);
        findNewFileImports(doc, snapshot, &importedFiles, &scannedPaths);
        findNewLibraryImports(doc, snapshot, modelManager, &importedFiles, &scannedPaths);

        // add new files to parse list
        foreach (const QString &file, importedFiles) {
            if (! files.contains(file))
                files.append(file);
344
345
        }

346
        modelManager->emitDocumentUpdated(doc);
347
348
    }

349
    future.setProgressValue(progressRange);
350
351
}

352
// Check whether fileMimeType is the same or extends knownMimeType
Roberto Raggi's avatar
Roberto Raggi committed
353
bool ModelManager::matchesMimeType(const Core::MimeType &fileMimeType, const Core::MimeType &knownMimeType)
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
{
    Core::MimeDatabase *db = Core::ICore::instance()->mimeDatabase();

    const QStringList knownTypeNames = QStringList(knownMimeType.type()) + knownMimeType.aliases();

    foreach (const QString knownTypeName, knownTypeNames)
        if (fileMimeType.matchesType(knownTypeName))
            return true;

    // recursion to parent types of fileMimeType
    foreach (const QString &parentMimeType, fileMimeType.subClassesOf()) {
        if (matchesMimeType(db->findByType(parentMimeType), knownMimeType))
            return true;
    }

    return false;
}
371
372
373
374
375

void ModelManager::setProjectImportPaths(const QStringList &importPaths)
{
    m_projectImportPaths = importPaths;

376
377
378
379
380
381
382
383
    // check if any file in the snapshot imports something new in the new paths
    Snapshot snapshot = _snapshot;
    QStringList importedFiles;
    QSet<QString> scannedPaths;
    foreach (const Document::Ptr &doc, snapshot)
        findNewLibraryImports(doc, snapshot, this, &importedFiles, &scannedPaths);

    updateSourceFiles(importedFiles);
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
}

QStringList ModelManager::importPaths() const
{
    QStringList paths;
    paths << m_projectImportPaths;
    paths << m_defaultImportPaths;
    return paths;
}

static QStringList environmentImportPaths()
{
    QStringList paths;

    QByteArray envImportPath = qgetenv("QML_IMPORT_PATH");

#if defined(Q_OS_WIN)
    QLatin1Char pathSep(';');
#else
    QLatin1Char pathSep(':');
#endif
    foreach (const QString &path, QString::fromLatin1(envImportPath).split(pathSep, QString::SkipEmptyParts)) {
        QString canonicalPath = QDir(path).canonicalPath();
        if (!canonicalPath.isEmpty() && !paths.contains(canonicalPath))
            paths.append(canonicalPath);
    }

    return paths;
}