1 /**************************************************************************
3 ** This file is part of Qt Creator
5 ** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies).
7 ** Contact: Nokia Corporation (qt-info@nokia.com)
11 ** This file contains pre-release code and may not be distributed.
12 ** You may use this file in accordance with the terms and conditions
13 ** contained in the Technology Preview License Agreement accompanying
16 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 2.1 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 2.1 requirements
23 ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
25 ** In addition, as a special exception, Nokia gives you certain additional
26 ** rights. These rights are described in the Nokia Qt LGPL Exception
27 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
29 ** If you have questions regarding the use of this file, please contact
30 ** Nokia at qt-info@nokia.com.
32 **************************************************************************/
35 #include "highlightdefinition.h"
36 #include "highlightdefinitionhandler.h"
37 #include "highlighterexception.h"
38 #include "definitiondownloader.h"
39 #include "highlightersettings.h"
40 #include "plaintexteditorfactory.h"
41 #include "texteditorconstants.h"
42 #include "texteditorplugin.h"
43 #include "texteditorsettings.h"
45 #include <coreplugin/icore.h>
46 #include <utils/qtcassert.h>
47 #include <coreplugin/progressmanager/progressmanager.h>
48 #include <qtconcurrent/QtConcurrentTools>
50 #include <QtCore/QtAlgorithms>
51 #include <QtCore/QtPlugin>
52 #include <QtCore/QString>
53 #include <QtCore/QLatin1Char>
54 #include <QtCore/QLatin1String>
55 #include <QtCore/QStringList>
56 #include <QtCore/QFile>
57 #include <QtCore/QFileInfo>
58 #include <QtCore/QDir>
59 #include <QtCore/QRegExp>
60 #include <QtCore/QFuture>
61 #include <QtCore/QtConcurrentRun>
62 #include <QtCore/QtConcurrentMap>
63 #include <QtCore/QUrl>
64 #include <QtCore/QSet>
65 #include <QtGui/QDesktopServices>
66 #include <QtGui/QMessageBox>
67 #include <QtXml/QXmlSimpleReader>
68 #include <QtXml/QXmlInputSource>
69 #include <QtXml/QXmlStreamReader>
70 #include <QtXml/QXmlStreamAttributes>
71 #include <QtNetwork/QNetworkRequest>
72 #include <QtNetwork/QNetworkReply>
73 #include <QtAlgorithms>
75 using namespace TextEditor;
76 using namespace Internal;
79 m_downloadingDefinitions(false),
80 m_registeringMimeTypes(false),
81 m_queuedMimeTypeRegistrations(0)
83 connect(&m_mimeTypeWatcher, SIGNAL(resultReadyAt(int)), this, SLOT(registerMimeType(int)));
84 connect(&m_mimeTypeWatcher, SIGNAL(finished()), this, SLOT(registerMimeTypesFinished()));
85 connect(&m_downloadWatcher, SIGNAL(finished()), this, SLOT(downloadDefinitionsFinished()));
91 Manager *Manager::instance()
93 static Manager manager;
97 QString Manager::definitionIdByName(const QString &name) const
98 { return m_idByName.value(name); }
100 QString Manager::definitionIdByMimeType(const QString &mimeType) const
101 { return m_idByMimeType.value(mimeType); }
103 QString Manager::definitionIdByAnyMimeType(const QStringList &mimeTypes) const
105 QString definitionId;
106 foreach (const QString &mimeType, mimeTypes) {
107 definitionId = definitionIdByMimeType(mimeType);
108 if (!definitionId.isEmpty())
114 QSharedPointer<HighlightDefinition> Manager::definition(const QString &id)
116 if (!id.isEmpty() && !m_definitions.contains(id)) {
117 QFile definitionFile(id);
118 if (!definitionFile.open(QIODevice::ReadOnly | QIODevice::Text))
119 return QSharedPointer<HighlightDefinition>();
121 QSharedPointer<HighlightDefinition> definition(new HighlightDefinition);
122 HighlightDefinitionHandler handler(definition);
124 QXmlInputSource source(&definitionFile);
125 QXmlSimpleReader reader;
126 reader.setContentHandler(&handler);
127 m_isBuilding.insert(id);
129 reader.parse(source);
130 } catch (HighlighterException &) {
133 m_isBuilding.remove(id);
134 definitionFile.close();
136 m_definitions.insert(id, definition);
139 return m_definitions.value(id);
142 QSharedPointer<HighlightDefinitionMetaData> Manager::definitionMetaData(const QString &id) const
143 { return m_definitionsMetaData.value(id); }
145 bool Manager::isBuildingDefinition(const QString &id) const
146 { return m_isBuilding.contains(id); }
148 void Manager::registerMimeTypes()
150 if (!m_registeringMimeTypes) {
151 m_registeringMimeTypes = true;
153 QFuture<Core::MimeType> future =
154 QtConcurrent::run(&Manager::gatherDefinitionsMimeTypes, this);
155 m_mimeTypeWatcher.setFuture(future);
156 Core::ICore::instance()->progressManager()->addTask(future,
157 tr("Registering definitions"),
158 Constants::TASK_REGISTER_DEFINITIONS);
160 // QFutures returned from QConcurrent::run cannot be cancelled. So the queue.
161 ++m_queuedMimeTypeRegistrations;
165 void Manager::gatherDefinitionsMimeTypes(QFutureInterface<Core::MimeType> &future)
167 // Please be aware of the following limitation in the current implementation.
168 // The generic highlighter only register its types after all other plugins
169 // have populated Creator's MIME database (so it does not override anything).
170 // When the generic highlighter settings change only its internal data is cleaned-up
171 // and rebuilt. Creator's MIME database is not touched. So depending on how the
172 // user plays around with the generic highlighter file definitions (changing
173 // duplicated patterns, for example), some changes might not be reflected.
174 // A definitive implementation would require some kind of re-load or update
175 // (considering hierarchies, aliases, etc) of the MIME database whenever there
176 // is a change in the generic highlighter settings.
178 QStringList definitionsPaths;
179 const HighlighterSettings &settings = TextEditorSettings::instance()->highlighterSettings();
180 definitionsPaths.append(settings.definitionFilesPath());
181 if (settings.useFallbackLocation())
182 definitionsPaths.append(settings.fallbackDefinitionFilesPath());
184 Core::MimeDatabase *mimeDatabase = Core::ICore::instance()->mimeDatabase();
185 QSet<QString> knownSuffixes = QSet<QString>::fromList(mimeDatabase->suffixes());
187 foreach (const QString &path, definitionsPaths) {
191 QDir definitionsDir(path);
192 QStringList filter(QLatin1String("*.xml"));
193 definitionsDir.setNameFilters(filter);
195 QList<QSharedPointer<HighlightDefinitionMetaData> > allMetaData;
196 const QFileInfoList &filesInfo = definitionsDir.entryInfoList();
197 foreach (const QFileInfo &fileInfo, filesInfo) {
198 const QSharedPointer<HighlightDefinitionMetaData> &metaData = parseMetadata(fileInfo);
199 if (!metaData.isNull())
200 allMetaData.append(metaData);
203 // Consider definitions with higher priority first.
204 qSort(allMetaData.begin(), allMetaData.end(), PriorityComp());
206 foreach (const QSharedPointer<HighlightDefinitionMetaData> &metaData, allMetaData) {
207 if (m_idByName.contains(metaData->name()))
208 // Name already exists... This is a fallback item, do not consider it.
211 const QString &id = metaData->id();
212 m_idByName.insert(metaData->name(), id);
213 m_definitionsMetaData.insert(id, metaData);
215 static const QStringList textPlain(QLatin1String("text/plain"));
217 // A definition can specify multiple MIME types and file extensions/patterns.
218 // However, each thing is done with a single string. There is no direct way to
219 // tell which patterns belong to which MIME types nor whether a MIME type is just
220 // an alias for the other. Currently, I associate all patterns with all MIME
221 // types from a definition.
222 QList<Core::MimeGlobPattern> globPatterns;
223 foreach (const QString &type, metaData->mimeTypes()) {
224 if (m_idByMimeType.contains(type))
227 m_idByMimeType.insert(type, id);
228 Core::MimeType mimeType = mimeDatabase->findByType(type);
229 if (mimeType.isNull()) {
230 if (globPatterns.isEmpty()) {
231 foreach (const QString &pattern, metaData->patterns()) {
232 static const QLatin1String mark("*.");
233 if (pattern.startsWith(mark)) {
234 const QString &suffix = pattern.right(pattern.length() - 2);
235 if (!knownSuffixes.contains(suffix))
236 knownSuffixes.insert(suffix);
240 QRegExp regExp(pattern, Qt::CaseSensitive, QRegExp::Wildcard);
241 globPatterns.append(Core::MimeGlobPattern(regExp, 50));
245 mimeType.setType(type);
246 mimeType.setSubClassesOf(textPlain);
247 mimeType.setComment(metaData->name());
248 mimeType.setGlobPatterns(globPatterns);
250 mimeDatabase->addMimeType(mimeType);
251 future.reportResult(mimeType);
258 void Manager::registerMimeType(int index) const
260 const Core::MimeType &mimeType = m_mimeTypeWatcher.resultAt(index);
261 TextEditorPlugin::instance()->editorFactory()->addMimeType(mimeType.type());
264 void Manager::registerMimeTypesFinished()
266 m_registeringMimeTypes = false;
267 if (m_queuedMimeTypeRegistrations > 0) {
268 --m_queuedMimeTypeRegistrations;
271 emit mimeTypesRegistered();
275 QSharedPointer<HighlightDefinitionMetaData> Manager::parseMetadata(const QFileInfo &fileInfo)
277 static const QLatin1Char kSemiColon(';');
278 static const QLatin1String kLanguage("language");
279 static const QLatin1String kArtificial("text/x-artificial-");
281 QFile definitionFile(fileInfo.absoluteFilePath());
282 if (!definitionFile.open(QIODevice::ReadOnly | QIODevice::Text))
283 return QSharedPointer<HighlightDefinitionMetaData>();
285 QSharedPointer<HighlightDefinitionMetaData> metaData(new HighlightDefinitionMetaData);
287 QXmlStreamReader reader(&definitionFile);
288 while (!reader.atEnd() && !reader.hasError()) {
289 if (reader.readNext() == QXmlStreamReader::StartElement && reader.name() == kLanguage) {
290 const QXmlStreamAttributes &atts = reader.attributes();
292 metaData->setFileName(fileInfo.fileName());
293 metaData->setId(fileInfo.absoluteFilePath());
294 metaData->setName(atts.value(HighlightDefinitionMetaData::kName).toString());
295 metaData->setVersion(atts.value(HighlightDefinitionMetaData::kVersion).toString());
296 metaData->setPriority(atts.value(HighlightDefinitionMetaData::kPriority).toString()
298 metaData->setPatterns(atts.value(HighlightDefinitionMetaData::kExtensions)
299 .toString().split(kSemiColon, QString::SkipEmptyParts));
301 QStringList mimeTypes = atts.value(HighlightDefinitionMetaData::kMimeType).
302 toString().split(kSemiColon, QString::SkipEmptyParts);
303 if (mimeTypes.isEmpty()) {
304 // There are definitions which do not specify a MIME type, but specify file
305 // patterns. Creating an artificial MIME type is a workaround.
306 QString artificialType(kArtificial);
307 artificialType.append(metaData->name());
308 mimeTypes.append(artificialType);
310 metaData->setMimeTypes(mimeTypes);
316 definitionFile.close();
321 QList<HighlightDefinitionMetaData> Manager::parseAvailableDefinitionsList(QIODevice *device) const
323 static const QLatin1Char kSlash('/');
324 static const QLatin1String kDefinition("Definition");
326 QList<HighlightDefinitionMetaData> metaDataList;
327 QXmlStreamReader reader(device);
328 while (!reader.atEnd() && !reader.hasError()) {
329 if (reader.readNext() == QXmlStreamReader::StartElement &&
330 reader.name() == kDefinition) {
331 const QXmlStreamAttributes &atts = reader.attributes();
333 HighlightDefinitionMetaData metaData;
334 metaData.setName(atts.value(HighlightDefinitionMetaData::kName).toString());
335 metaData.setVersion(atts.value(HighlightDefinitionMetaData::kVersion).toString());
336 QString url(atts.value(HighlightDefinitionMetaData::kUrl).toString());
337 metaData.setUrl(QUrl(url));
338 const int slash = url.lastIndexOf(kSlash);
340 metaData.setFileName(url.right(url.length() - slash - 1));
342 metaDataList.append(metaData);
350 void Manager::downloadAvailableDefinitionsMetaData()
352 QUrl url(QLatin1String("http://www.kate-editor.org/syntax/update-3.2.xml"));
353 QNetworkRequest request(url);
354 // Currently this takes a couple of seconds on Windows 7: QTBUG-10106.
355 QNetworkReply *reply = m_networkManager.get(request);
356 connect(reply, SIGNAL(finished()), this, SLOT(downloadAvailableDefinitionsListFinished()));
359 void Manager::downloadAvailableDefinitionsListFinished()
361 if (QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender())) {
362 if (reply->error() == QNetworkReply::NoError)
363 emit definitionsMetaDataReady(parseAvailableDefinitionsList(reply));
365 emit errorDownloadingDefinitionsMetaData();
366 reply->deleteLater();
370 void Manager::downloadDefinitions(const QList<QUrl> &urls, const QString &savePath)
372 m_downloaders.clear();
373 foreach (const QUrl &url, urls)
374 m_downloaders.append(new DefinitionDownloader(url, savePath));
376 m_downloadingDefinitions = true;
377 QFuture<void> future = QtConcurrent::map(m_downloaders, DownloaderStarter());
378 m_downloadWatcher.setFuture(future);
379 Core::ICore::instance()->progressManager()->addTask(future,
380 tr("Downloading definitions"),
381 Constants::TASK_DOWNLOAD_DEFINITIONS);
384 void Manager::downloadDefinitionsFinished()
387 bool writeError = false;
388 foreach (DefinitionDownloader *downloader, m_downloaders) {
389 DefinitionDownloader::Status status = downloader->status();
390 if (status != DefinitionDownloader::Ok) {
392 if (status == DefinitionDownloader::WriteError && !writeError)
400 if (errors == m_downloaders.size())
401 text = tr("Error downloading selected definition(s).");
403 text = tr("Error downloading one or more definitions.");
405 text.append(tr("\nPlease check the directory's access rights."));
406 QMessageBox::critical(0, tr("Download Error"), text);
409 m_downloadingDefinitions = false;
412 bool Manager::isDownloadingDefinitions() const
414 return m_downloadingDefinitions;
417 void Manager::clear()
420 m_idByMimeType.clear();
421 m_definitions.clear();
422 m_definitionsMetaData.clear();