forked from qt-creator/qt-creator
Currently the QmlJSEditorFactory hardcodes that .qml.ui files are opened "in Design mode", without any option to explicitly open them directly in text mode. While this is the preferred way of working, there are always exceptions. Separate the "Edit mode by default" and the "Design mode by default" parts into separate editor factories. The former one is the default editor for .qml files, the latter for .qml.ui files. This allows users to explicitly choose "Open With > QML JS Editor" to open .qml.ui files directly in Edit mode. Fixes: QTCREATORBUG-18123 Change-Id: I72ab2d25fdc538210123782f6611f9c6e3157dea Reviewed-by: Christian Stenger <christian.stenger@qt.io> Reviewed-by: Thomas Hartmann <thomas.hartmann@qt.io>
730 lines
21 KiB
C++
730 lines
21 KiB
C++
/****************************************************************************
|
|
**
|
|
** Copyright (C) 2016 The Qt Company Ltd.
|
|
** Contact: https://www.qt.io/licensing/
|
|
**
|
|
** This file is part of Qt Creator.
|
|
**
|
|
** 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 The Qt Company. For licensing terms
|
|
** and conditions see https://www.qt.io/terms-conditions. For further
|
|
** information use the contact form at https://www.qt.io/contact-us.
|
|
**
|
|
** GNU General Public License Usage
|
|
** Alternatively, this file may be used under the terms of the GNU
|
|
** General Public License version 3 as published by the Free Software
|
|
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
|
|
** included in the packaging of this file. Please review the following
|
|
** information to ensure the GNU General Public License requirements will
|
|
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
|
|
**
|
|
****************************************************************************/
|
|
|
|
#include "qmljseditordocument.h"
|
|
|
|
#include "qmljseditorconstants.h"
|
|
#include "qmljseditordocument_p.h"
|
|
#include "qmljseditorplugin.h"
|
|
#include "qmljshighlighter.h"
|
|
#include "qmljsquickfixassist.h"
|
|
#include "qmljssemantichighlighter.h"
|
|
#include "qmljssemanticinfoupdater.h"
|
|
#include "qmljstextmark.h"
|
|
#include "qmloutlinemodel.h"
|
|
|
|
#include <coreplugin/coreconstants.h>
|
|
#include <coreplugin/infobar.h>
|
|
#include <coreplugin/modemanager.h>
|
|
|
|
#include <qmljstools/qmljsindenter.h>
|
|
#include <qmljstools/qmljsmodelmanager.h>
|
|
#include <qmljstools/qmljsqtstylecodeformatter.h>
|
|
|
|
const char QML_UI_FILE_WARNING[] = "QmlJSEditor.QmlUiFileWarning";
|
|
|
|
using namespace QmlJSEditor;
|
|
using namespace QmlJS;
|
|
using namespace QmlJS::AST;
|
|
using namespace QmlJSTools;
|
|
|
|
namespace {
|
|
|
|
enum {
|
|
UPDATE_DOCUMENT_DEFAULT_INTERVAL = 100,
|
|
UPDATE_OUTLINE_INTERVAL = 500
|
|
};
|
|
|
|
struct Declaration
|
|
{
|
|
QString text;
|
|
int startLine = 0;
|
|
int startColumn = 0;
|
|
int endLine = 0;
|
|
int endColumn = 0;
|
|
};
|
|
|
|
class FindIdDeclarations: protected Visitor
|
|
{
|
|
public:
|
|
using Result = QHash<QString, QList<AST::SourceLocation> >;
|
|
|
|
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.isEmpty())
|
|
text += id->name;
|
|
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;
|
|
|
|
bool visit(AST::UiScriptBinding *node) override
|
|
{
|
|
if (asString(node->qualifiedId) == QLatin1String("id")) {
|
|
if (auto stmt = AST::cast<const AST::ExpressionStatement*>(node->statement)) {
|
|
if (auto idExpr = AST::cast<const AST::IdentifierExpression *>(stmt->expression)) {
|
|
if (!idExpr->name.isEmpty()) {
|
|
const QString &id = idExpr->name.toString();
|
|
QList<AST::SourceLocation> *locs = &_ids[id];
|
|
locs->append(idExpr->firstSourceLocation());
|
|
locs->append(_maybeIds.value(id));
|
|
_maybeIds.remove(id);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
accept(node->statement);
|
|
|
|
return false;
|
|
}
|
|
|
|
bool visit(AST::IdentifierExpression *node) override
|
|
{
|
|
if (!node->name.isEmpty()) {
|
|
const QString &name = node->name.toString();
|
|
|
|
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<Declaration> _declarations;
|
|
int _depth;
|
|
|
|
public:
|
|
QList<Declaration> 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.isEmpty())
|
|
text += id->name;
|
|
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;
|
|
}
|
|
|
|
bool visit(AST::UiObjectDefinition *node) override
|
|
{
|
|
++_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
|
|
}
|
|
|
|
void endVisit(AST::UiObjectDefinition *) override
|
|
{
|
|
--_depth;
|
|
}
|
|
|
|
bool visit(AST::UiObjectBinding *node) override
|
|
{
|
|
++_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
|
|
}
|
|
|
|
void endVisit(AST::UiObjectBinding *) override
|
|
{
|
|
--_depth;
|
|
}
|
|
|
|
bool visit(AST::UiScriptBinding *) override
|
|
{
|
|
++_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.
|
|
}
|
|
|
|
void endVisit(AST::UiScriptBinding *) override
|
|
{
|
|
--_depth;
|
|
}
|
|
|
|
bool visit(AST::FunctionExpression *) override
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool visit(AST::FunctionDeclaration *ast) override
|
|
{
|
|
if (ast->name.isEmpty())
|
|
return false;
|
|
|
|
Declaration decl;
|
|
init(&decl, ast);
|
|
|
|
decl.text.fill(QLatin1Char(' '), _depth);
|
|
decl.text += ast->name;
|
|
|
|
decl.text += QLatin1Char('(');
|
|
for (FormalParameterList *it = ast->formals; it; it = it->next) {
|
|
if (!it->element->bindingIdentifier.isEmpty())
|
|
decl.text += it->element->bindingIdentifier;
|
|
|
|
if (it->next)
|
|
decl.text += QLatin1String(", ");
|
|
}
|
|
|
|
decl.text += QLatin1Char(')');
|
|
|
|
_declarations.append(decl);
|
|
|
|
return false;
|
|
}
|
|
|
|
bool visit(AST::PatternElement *ast) override
|
|
{
|
|
if (!ast->isVariableDeclaration() || ast->bindingIdentifier.isEmpty())
|
|
return false;
|
|
|
|
Declaration decl;
|
|
decl.text.fill(QLatin1Char(' '), _depth);
|
|
decl.text += ast->bindingIdentifier;
|
|
|
|
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;
|
|
}
|
|
|
|
bool visit(AST::BinaryExpression *ast) override
|
|
{
|
|
auto field = AST::cast<const AST::FieldMemberExpression *>(ast->left);
|
|
auto funcExpr = AST::cast<const AST::FunctionExpression *>(ast->right);
|
|
|
|
if (field && funcExpr && funcExpr->body && (ast->op == QSOperator::Assign)) {
|
|
Declaration decl;
|
|
init(&decl, ast);
|
|
|
|
decl.text.fill(QLatin1Char(' '), _depth);
|
|
decl.text += field->name;
|
|
|
|
decl.text += QLatin1Char('(');
|
|
for (FormalParameterList *it = funcExpr->formals; it; it = it->next) {
|
|
if (!it->element->bindingIdentifier.isEmpty())
|
|
decl.text += it->element->bindingIdentifier;
|
|
|
|
if (it->next)
|
|
decl.text += QLatin1String(", ");
|
|
}
|
|
decl.text += QLatin1Char(')');
|
|
|
|
_declarations.append(decl);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
};
|
|
|
|
class CreateRanges: protected AST::Visitor
|
|
{
|
|
QTextDocument *_textDocument;
|
|
QList<Range> _ranges;
|
|
|
|
public:
|
|
QList<Range> operator()(QTextDocument *textDocument, Document::Ptr doc)
|
|
{
|
|
_textDocument = textDocument;
|
|
_ranges.clear();
|
|
if (doc && doc->ast() != nullptr)
|
|
doc->ast()->accept(this);
|
|
return _ranges;
|
|
}
|
|
|
|
protected:
|
|
using AST::Visitor::visit;
|
|
|
|
bool visit(AST::UiObjectBinding *ast) override
|
|
{
|
|
if (ast->initializer && ast->initializer->lbraceToken.length)
|
|
_ranges.append(createRange(ast, ast->initializer));
|
|
return true;
|
|
}
|
|
|
|
bool visit(AST::UiObjectDefinition *ast) override
|
|
{
|
|
if (ast->initializer && ast->initializer->lbraceToken.length)
|
|
_ranges.append(createRange(ast, ast->initializer));
|
|
return true;
|
|
}
|
|
|
|
bool visit(AST::FunctionExpression *ast) override
|
|
{
|
|
_ranges.append(createRange(ast));
|
|
return true;
|
|
}
|
|
|
|
bool visit(AST::FunctionDeclaration *ast) override
|
|
{
|
|
_ranges.append(createRange(ast));
|
|
return true;
|
|
}
|
|
|
|
bool visit(AST::BinaryExpression *ast) override
|
|
{
|
|
auto field = AST::cast<AST::FieldMemberExpression *>(ast->left);
|
|
auto funcExpr = AST::cast<AST::FunctionExpression *>(ast->right);
|
|
|
|
if (field && funcExpr && funcExpr->body && (ast->op == QSOperator::Assign))
|
|
_ranges.append(createRange(ast, ast->firstSourceLocation(), ast->lastSourceLocation()));
|
|
return true;
|
|
}
|
|
|
|
bool visit(AST::UiScriptBinding *ast) override
|
|
{
|
|
if (auto block = AST::cast<AST::Block *>(ast->statement))
|
|
_ranges.append(createRange(ast, block));
|
|
return true;
|
|
}
|
|
|
|
Range createRange(AST::UiObjectMember *member, AST::UiObjectInitializer *ast)
|
|
{
|
|
return createRange(member, member->firstSourceLocation(), ast->rbraceToken);
|
|
}
|
|
|
|
Range createRange(AST::FunctionExpression *ast)
|
|
{
|
|
return createRange(ast, ast->lbraceToken, ast->rbraceToken);
|
|
}
|
|
|
|
Range createRange(AST::UiScriptBinding *ast, AST::Block *block)
|
|
{
|
|
return createRange(ast, block->lbraceToken, block->rbraceToken);
|
|
}
|
|
|
|
Range createRange(AST::Node *ast, AST::SourceLocation start, AST::SourceLocation end)
|
|
{
|
|
Range range;
|
|
|
|
range.ast = ast;
|
|
|
|
range.begin = QTextCursor(_textDocument);
|
|
range.begin.setPosition(start.begin());
|
|
|
|
range.end = QTextCursor(_textDocument);
|
|
range.end.setPosition(end.end());
|
|
|
|
return range;
|
|
}
|
|
};
|
|
|
|
}
|
|
|
|
namespace QmlJSEditor {
|
|
namespace Internal {
|
|
|
|
QmlJSEditorDocumentPrivate::QmlJSEditorDocumentPrivate(QmlJSEditorDocument *parent)
|
|
: q(parent)
|
|
, m_semanticHighlighter(new SemanticHighlighter(parent))
|
|
, m_outlineModel(new QmlOutlineModel(parent))
|
|
{
|
|
ModelManagerInterface *modelManager = ModelManagerInterface::instance();
|
|
|
|
// code model
|
|
m_updateDocumentTimer.setInterval(UPDATE_DOCUMENT_DEFAULT_INTERVAL);
|
|
m_updateDocumentTimer.setSingleShot(true);
|
|
connect(q->document(), &QTextDocument::contentsChanged,
|
|
&m_updateDocumentTimer, QOverload<>::of(&QTimer::start));
|
|
connect(&m_updateDocumentTimer, &QTimer::timeout,
|
|
this, &QmlJSEditorDocumentPrivate::reparseDocument);
|
|
connect(modelManager, &ModelManagerInterface::documentUpdated,
|
|
this, &QmlJSEditorDocumentPrivate::onDocumentUpdated);
|
|
|
|
// semantic info
|
|
m_semanticInfoUpdater = new SemanticInfoUpdater(this);
|
|
connect(m_semanticInfoUpdater, &SemanticInfoUpdater::updated,
|
|
this, &QmlJSEditorDocumentPrivate::acceptNewSemanticInfo);
|
|
m_semanticInfoUpdater->start();
|
|
|
|
// library info changes
|
|
m_reupdateSemanticInfoTimer.setInterval(UPDATE_DOCUMENT_DEFAULT_INTERVAL);
|
|
m_reupdateSemanticInfoTimer.setSingleShot(true);
|
|
connect(&m_reupdateSemanticInfoTimer, &QTimer::timeout,
|
|
this, &QmlJSEditorDocumentPrivate::reupdateSemanticInfo);
|
|
connect(modelManager, &ModelManagerInterface::libraryInfoUpdated,
|
|
&m_reupdateSemanticInfoTimer, QOverload<>::of(&QTimer::start));
|
|
|
|
// outline model
|
|
m_updateOutlineModelTimer.setInterval(UPDATE_OUTLINE_INTERVAL);
|
|
m_updateOutlineModelTimer.setSingleShot(true);
|
|
connect(&m_updateOutlineModelTimer, &QTimer::timeout,
|
|
this, &QmlJSEditorDocumentPrivate::updateOutlineModel);
|
|
|
|
modelManager->updateSourceFiles(QStringList(parent->filePath().toString()), false);
|
|
}
|
|
|
|
QmlJSEditorDocumentPrivate::~QmlJSEditorDocumentPrivate()
|
|
{
|
|
m_semanticInfoUpdater->abort();
|
|
m_semanticInfoUpdater->wait();
|
|
// clean up all marks, otherwise a callback could try to access deleted members.
|
|
// see QTCREATORBUG-20199
|
|
cleanDiagnosticMarks();
|
|
cleanSemanticMarks();
|
|
}
|
|
|
|
void QmlJSEditorDocumentPrivate::invalidateFormatterCache()
|
|
{
|
|
CreatorCodeFormatter formatter(q->tabSettings());
|
|
formatter.invalidateCache(q->document());
|
|
}
|
|
|
|
void QmlJSEditorDocumentPrivate::reparseDocument()
|
|
{
|
|
ModelManagerInterface::instance()->updateSourceFiles(QStringList(q->filePath().toString()),
|
|
false);
|
|
}
|
|
|
|
void QmlJSEditorDocumentPrivate::onDocumentUpdated(Document::Ptr doc)
|
|
{
|
|
if (q->filePath().toString() != doc->fileName())
|
|
return;
|
|
|
|
// text document has changed, simply wait for the next onDocumentUpdated
|
|
if (doc->editorRevision() != q->document()->revision())
|
|
return;
|
|
|
|
cleanDiagnosticMarks();
|
|
if (doc->ast()) {
|
|
// got a correctly parsed (or recovered) file.
|
|
m_semanticInfoDocRevision = doc->editorRevision();
|
|
m_semanticInfoUpdater->update(doc, ModelManagerInterface::instance()->snapshot());
|
|
} else if (doc->language().isFullySupportedLanguage()) {
|
|
createTextMarks(doc->diagnosticMessages());
|
|
}
|
|
emit q->updateCodeWarnings(doc);
|
|
}
|
|
|
|
void QmlJSEditorDocumentPrivate::reupdateSemanticInfo()
|
|
{
|
|
// If the editor is newer than the semantic info (possibly with update in progress),
|
|
// new semantic infos won't be accepted anyway. We'll get a onDocumentUpdated anyhow.
|
|
if (q->document()->revision() != m_semanticInfoDocRevision)
|
|
return;
|
|
|
|
m_semanticInfoUpdater->reupdate(ModelManagerInterface::instance()->snapshot());
|
|
}
|
|
|
|
void QmlJSEditorDocumentPrivate::acceptNewSemanticInfo(const SemanticInfo &semanticInfo)
|
|
{
|
|
if (semanticInfo.revision() != q->document()->revision()) {
|
|
// ignore outdated semantic infos
|
|
return;
|
|
}
|
|
|
|
m_semanticInfo = semanticInfo;
|
|
Document::Ptr doc = semanticInfo.document;
|
|
|
|
// create the ranges
|
|
CreateRanges createRanges;
|
|
m_semanticInfo.ranges = createRanges(q->document(), doc);
|
|
|
|
// Refresh the ids
|
|
FindIdDeclarations updateIds;
|
|
m_semanticInfo.idLocations = updateIds(doc);
|
|
|
|
m_outlineModelNeedsUpdate = true;
|
|
m_semanticHighlightingNecessary = true;
|
|
|
|
createTextMarks(m_semanticInfo);
|
|
emit q->semanticInfoUpdated(m_semanticInfo); // calls triggerPendingUpdates as necessary
|
|
}
|
|
|
|
void QmlJSEditorDocumentPrivate::updateOutlineModel()
|
|
{
|
|
if (q->isSemanticInfoOutdated())
|
|
return; // outline update will be retriggered when semantic info is updated
|
|
|
|
m_outlineModel->update(m_semanticInfo);
|
|
}
|
|
|
|
static void cleanMarks(QVector<TextEditor::TextMark *> *marks, TextEditor::TextDocument *doc)
|
|
{
|
|
// if doc is null, this method is improperly called, so better do nothing that leave an
|
|
// inconsistent state where marks are cleared but not removed from doc.
|
|
if (!marks || !doc)
|
|
return;
|
|
for (TextEditor::TextMark *mark : *marks) {
|
|
doc->removeMark(mark);
|
|
delete mark;
|
|
}
|
|
marks->clear();
|
|
}
|
|
|
|
void QmlJSEditorDocumentPrivate::createTextMarks(const QList<DiagnosticMessage> &diagnostics)
|
|
{
|
|
for (const DiagnosticMessage &diagnostic : diagnostics) {
|
|
const auto onMarkRemoved = [this](QmlJSTextMark *mark) {
|
|
m_diagnosticMarks.removeAll(mark);
|
|
delete mark;
|
|
};
|
|
|
|
auto mark = new QmlJSTextMark(q->filePath(), diagnostic, onMarkRemoved);
|
|
m_diagnosticMarks.append(mark);
|
|
q->addMark(mark);
|
|
}
|
|
}
|
|
|
|
void QmlJSEditorDocumentPrivate::cleanDiagnosticMarks()
|
|
{
|
|
cleanMarks(&m_diagnosticMarks, q);
|
|
}
|
|
|
|
void QmlJSEditorDocumentPrivate::createTextMarks(const SemanticInfo &info)
|
|
{
|
|
cleanSemanticMarks();
|
|
const auto onMarkRemoved = [this](QmlJSTextMark *mark) {
|
|
m_semanticMarks.removeAll(mark);
|
|
delete mark;
|
|
};
|
|
for (const DiagnosticMessage &diagnostic : qAsConst(info.semanticMessages)) {
|
|
auto mark = new QmlJSTextMark(q->filePath(),
|
|
diagnostic, onMarkRemoved);
|
|
m_semanticMarks.append(mark);
|
|
q->addMark(mark);
|
|
}
|
|
for (const QmlJS::StaticAnalysis::Message &message : qAsConst(info.staticAnalysisMessages)) {
|
|
auto mark = new QmlJSTextMark(q->filePath(),
|
|
message, onMarkRemoved);
|
|
m_semanticMarks.append(mark);
|
|
q->addMark(mark);
|
|
}
|
|
}
|
|
|
|
void QmlJSEditorDocumentPrivate::cleanSemanticMarks()
|
|
{
|
|
cleanMarks(&m_semanticMarks, q);
|
|
}
|
|
|
|
} // Internal
|
|
|
|
QmlJSEditorDocument::QmlJSEditorDocument(Core::Id id)
|
|
: d(new Internal::QmlJSEditorDocumentPrivate(this))
|
|
{
|
|
setId(id);
|
|
connect(this, &TextEditor::TextDocument::tabSettingsChanged,
|
|
d, &Internal::QmlJSEditorDocumentPrivate::invalidateFormatterCache);
|
|
setSyntaxHighlighter(new QmlJSHighlighter(document()));
|
|
setIndenter(new Internal::Indenter(document()));
|
|
}
|
|
|
|
QmlJSEditorDocument::~QmlJSEditorDocument()
|
|
{
|
|
delete d;
|
|
}
|
|
|
|
const SemanticInfo &QmlJSEditorDocument::semanticInfo() const
|
|
{
|
|
return d->m_semanticInfo;
|
|
}
|
|
|
|
bool QmlJSEditorDocument::isSemanticInfoOutdated() const
|
|
{
|
|
return d->m_semanticInfo.revision() != document()->revision();
|
|
}
|
|
|
|
QVector<QTextLayout::FormatRange> QmlJSEditorDocument::diagnosticRanges() const
|
|
{
|
|
return d->m_diagnosticRanges;
|
|
}
|
|
|
|
Internal::QmlOutlineModel *QmlJSEditorDocument::outlineModel() const
|
|
{
|
|
return d->m_outlineModel;
|
|
}
|
|
|
|
TextEditor::IAssistProvider *QmlJSEditorDocument::quickFixAssistProvider() const
|
|
{
|
|
return Internal::QmlJSEditorPlugin::quickFixAssistProvider();
|
|
}
|
|
|
|
void QmlJSEditorDocument::setIsDesignModePreferred(bool value)
|
|
{
|
|
d->m_isDesignModePreferred = value;
|
|
if (value) {
|
|
if (infoBar()->canInfoBeAdded(QML_UI_FILE_WARNING)) {
|
|
Core::InfoBarEntry info(QML_UI_FILE_WARNING,
|
|
tr("This file should only be edited in <b>Design</b> mode."));
|
|
info.setCustomButtonInfo(tr("Switch Mode"), []() {
|
|
Core::ModeManager::activateMode(Core::Constants::MODE_DESIGN);
|
|
});
|
|
infoBar()->addInfo(info);
|
|
}
|
|
} else if (infoBar()->containsInfo(QML_UI_FILE_WARNING)) {
|
|
infoBar()->removeInfo(QML_UI_FILE_WARNING);
|
|
}
|
|
}
|
|
|
|
bool QmlJSEditorDocument::isDesignModePreferred() const
|
|
{
|
|
return d->m_isDesignModePreferred;
|
|
}
|
|
|
|
void QmlJSEditorDocument::setDiagnosticRanges(const QVector<QTextLayout::FormatRange> &ranges)
|
|
{
|
|
d->m_diagnosticRanges = ranges;
|
|
}
|
|
|
|
void QmlJSEditorDocument::applyFontSettings()
|
|
{
|
|
TextDocument::applyFontSettings();
|
|
d->m_semanticHighlighter->updateFontSettings(fontSettings());
|
|
if (!isSemanticInfoOutdated()) {
|
|
d->m_semanticHighlightingNecessary = false;
|
|
d->m_semanticHighlighter->rerun(d->m_semanticInfo);
|
|
}
|
|
}
|
|
|
|
void QmlJSEditorDocument::triggerPendingUpdates()
|
|
{
|
|
TextDocument::triggerPendingUpdates(); // calls applyFontSettings if necessary
|
|
// might still need to rehighlight if font settings did not change
|
|
if (d->m_semanticHighlightingNecessary && !isSemanticInfoOutdated()) {
|
|
d->m_semanticHighlightingNecessary = false;
|
|
d->m_semanticHighlighter->rerun(d->m_semanticInfo);
|
|
}
|
|
if (d->m_outlineModelNeedsUpdate && !isSemanticInfoOutdated()) {
|
|
d->m_outlineModelNeedsUpdate = false;
|
|
d->m_updateOutlineModelTimer.start();
|
|
}
|
|
}
|
|
|
|
} // QmlJSEditor
|