forked from qt-creator/qt-creator
Add QmlJS semantic checker.
* Add SemanticHighlighter to QmlJSTextEditor to update the semantic info in a background thread. * Add QmlJS::Check to run semantic checks on qml and js documents. * Add a check for incorrect property names. * Fix hoverhandler to show tool tips from extra selections over help tooltips.
This commit is contained in:
@@ -36,6 +36,7 @@
|
||||
#include <qmljs/qmljsindenter.h>
|
||||
#include <qmljs/qmljsinterpreter.h>
|
||||
#include <qmljs/qmljsbind.h>
|
||||
#include <qmljs/qmljscheck.h>
|
||||
#include <qmljs/qmljsevaluate.h>
|
||||
#include <qmljs/qmljsdocument.h>
|
||||
#include <qmljs/parser/qmljsastvisitor_p.h>
|
||||
@@ -563,6 +564,11 @@ QmlJSTextEditor::QmlJSTextEditor(QWidget *parent) :
|
||||
m_methodCombo(0),
|
||||
m_modelManager(0)
|
||||
{
|
||||
qRegisterMetaType<SemanticInfo>("SemanticInfo");
|
||||
|
||||
m_semanticHighlighter = new SemanticHighlighter(this);
|
||||
m_semanticHighlighter->start();
|
||||
|
||||
setParenthesesMatchingEnabled(true);
|
||||
setMarksVisible(true);
|
||||
setCodeFoldingSupported(true);
|
||||
@@ -589,10 +595,15 @@ QmlJSTextEditor::QmlJSTextEditor(QWidget *parent) :
|
||||
connect(m_modelManager, SIGNAL(documentUpdated(QmlJS::Document::Ptr)),
|
||||
this, SLOT(onDocumentUpdated(QmlJS::Document::Ptr)));
|
||||
}
|
||||
|
||||
connect(m_semanticHighlighter, SIGNAL(changed(SemanticInfo)),
|
||||
this, SLOT(updateSemanticInfo(SemanticInfo)));
|
||||
}
|
||||
|
||||
QmlJSTextEditor::~QmlJSTextEditor()
|
||||
{
|
||||
m_semanticHighlighter->abort();
|
||||
m_semanticHighlighter->wait();
|
||||
}
|
||||
|
||||
SemanticInfo QmlJSTextEditor::semanticInfo() const
|
||||
@@ -660,62 +671,9 @@ void QmlJSTextEditor::onDocumentUpdated(QmlJS::Document::Ptr doc)
|
||||
if (doc->ast()) {
|
||||
// got a correctly parsed (or recovered) file.
|
||||
|
||||
// create the ranges and update the semantic info.
|
||||
CreateRanges createRanges;
|
||||
SemanticInfo sem;
|
||||
sem.snapshot = m_modelManager->snapshot();
|
||||
sem.document = doc;
|
||||
sem.ranges = createRanges(document(), doc);
|
||||
|
||||
// Refresh the ids
|
||||
FindIdDeclarations updateIds;
|
||||
sem.idLocations = updateIds(doc);
|
||||
|
||||
if (doc->isParsedCorrectly()) {
|
||||
FindDeclarations findDeclarations;
|
||||
sem.declarations = findDeclarations(doc->ast());
|
||||
|
||||
QStringList items;
|
||||
items.append(tr("<Select Symbol>"));
|
||||
|
||||
foreach (Declaration decl, sem.declarations)
|
||||
items.append(decl.text);
|
||||
|
||||
m_methodCombo->clear();
|
||||
m_methodCombo->addItems(items);
|
||||
updateMethodBoxIndex();
|
||||
}
|
||||
|
||||
m_semanticInfo = sem;
|
||||
const SemanticHighlighter::Source source = currentSource(/*force = */ true);
|
||||
m_semanticHighlighter->rehighlight(source);
|
||||
}
|
||||
|
||||
QList<QTextEdit::ExtraSelection> selections;
|
||||
|
||||
foreach (const DiagnosticMessage &d, doc->diagnosticMessages()) {
|
||||
if (d.isWarning())
|
||||
continue;
|
||||
|
||||
const int line = d.loc.startLine;
|
||||
const int column = qMax(1U, d.loc.startColumn);
|
||||
|
||||
QTextEdit::ExtraSelection sel;
|
||||
QTextCursor c(document()->findBlockByNumber(line - 1));
|
||||
sel.cursor = c;
|
||||
|
||||
sel.cursor.setPosition(c.position() + column - 1);
|
||||
if (sel.cursor.atBlockEnd())
|
||||
sel.cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor);
|
||||
else
|
||||
sel.cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
|
||||
|
||||
sel.format.setUnderlineColor(Qt::red);
|
||||
sel.format.setUnderlineStyle(QTextCharFormat::WaveUnderline);
|
||||
sel.format.setToolTip(d.message);
|
||||
|
||||
selections.append(sel);
|
||||
}
|
||||
|
||||
setExtraSelections(CodeWarningsSelection, selections);
|
||||
}
|
||||
|
||||
void QmlJSTextEditor::jumpToMethod(int index)
|
||||
@@ -1150,3 +1108,196 @@ QString QmlJSTextEditor::insertParagraphSeparator(const QTextCursor &) const
|
||||
return QLatin1String("}\n");
|
||||
}
|
||||
|
||||
static void appendExtraSelectionsForMessages(
|
||||
QList<QTextEdit::ExtraSelection> *selections,
|
||||
const QList<DiagnosticMessage> &messages,
|
||||
const QTextDocument *document)
|
||||
{
|
||||
foreach (const DiagnosticMessage &d, messages) {
|
||||
if (d.isWarning())
|
||||
continue;
|
||||
|
||||
const int line = d.loc.startLine;
|
||||
const int column = qMax(1U, d.loc.startColumn);
|
||||
|
||||
QTextEdit::ExtraSelection sel;
|
||||
QTextCursor c(document->findBlockByNumber(line - 1));
|
||||
sel.cursor = c;
|
||||
|
||||
sel.cursor.setPosition(c.position() + column - 1);
|
||||
if (sel.cursor.atBlockEnd())
|
||||
sel.cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::KeepAnchor);
|
||||
else
|
||||
sel.cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
|
||||
|
||||
sel.format.setUnderlineColor(Qt::red);
|
||||
sel.format.setUnderlineStyle(QTextCharFormat::WaveUnderline);
|
||||
sel.format.setToolTip(d.message);
|
||||
|
||||
selections->append(sel);
|
||||
}
|
||||
}
|
||||
|
||||
void QmlJSTextEditor::semanticRehighlight()
|
||||
{
|
||||
m_semanticHighlighter->rehighlight(currentSource());
|
||||
}
|
||||
|
||||
void QmlJSTextEditor::updateSemanticInfo(const SemanticInfo &semanticInfo)
|
||||
{
|
||||
if (semanticInfo.revision() != document()->revision()) {
|
||||
// got outdated semantic info
|
||||
semanticRehighlight();
|
||||
return;
|
||||
}
|
||||
|
||||
m_semanticInfo = semanticInfo;
|
||||
Document::Ptr doc = semanticInfo.document;
|
||||
|
||||
// create the ranges
|
||||
CreateRanges createRanges;
|
||||
m_semanticInfo.ranges = createRanges(document(), doc);
|
||||
|
||||
// Refresh the ids
|
||||
FindIdDeclarations updateIds;
|
||||
m_semanticInfo.idLocations = updateIds(doc);
|
||||
|
||||
if (doc->isParsedCorrectly()) {
|
||||
FindDeclarations findDeclarations;
|
||||
m_semanticInfo.declarations = findDeclarations(doc->ast());
|
||||
|
||||
QStringList items;
|
||||
items.append(tr("<Select Symbol>"));
|
||||
|
||||
foreach (Declaration decl, m_semanticInfo.declarations)
|
||||
items.append(decl.text);
|
||||
|
||||
m_methodCombo->clear();
|
||||
m_methodCombo->addItems(items);
|
||||
updateMethodBoxIndex();
|
||||
}
|
||||
|
||||
// update warning/error extra selections
|
||||
QList<QTextEdit::ExtraSelection> selections;
|
||||
appendExtraSelectionsForMessages(&selections, doc->diagnosticMessages(), document());
|
||||
appendExtraSelectionsForMessages(&selections, m_semanticInfo.semanticMessages, document());
|
||||
setExtraSelections(CodeWarningsSelection, selections);
|
||||
}
|
||||
|
||||
SemanticHighlighter::Source QmlJSTextEditor::currentSource(bool force)
|
||||
{
|
||||
int line = 0, column = 0;
|
||||
convertPosition(position(), &line, &column);
|
||||
|
||||
const Snapshot snapshot = m_modelManager->snapshot();
|
||||
const QString fileName = file()->fileName();
|
||||
|
||||
QString code;
|
||||
if (force || m_semanticInfo.revision() != document()->revision())
|
||||
code = toPlainText(); // get the source code only when needed.
|
||||
|
||||
const unsigned revision = document()->revision();
|
||||
SemanticHighlighter::Source source(snapshot, fileName, code,
|
||||
line, column, revision);
|
||||
source.force = force;
|
||||
return source;
|
||||
}
|
||||
|
||||
SemanticHighlighter::SemanticHighlighter(QObject *parent)
|
||||
: QThread(parent),
|
||||
m_done(false)
|
||||
{
|
||||
}
|
||||
|
||||
SemanticHighlighter::~SemanticHighlighter()
|
||||
{
|
||||
}
|
||||
|
||||
void SemanticHighlighter::abort()
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
m_done = true;
|
||||
m_condition.wakeOne();
|
||||
}
|
||||
|
||||
void SemanticHighlighter::rehighlight(const Source &source)
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
m_source = source;
|
||||
m_condition.wakeOne();
|
||||
}
|
||||
|
||||
bool SemanticHighlighter::isOutdated()
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
const bool outdated = ! m_source.fileName.isEmpty() || m_done;
|
||||
return outdated;
|
||||
}
|
||||
|
||||
void SemanticHighlighter::run()
|
||||
{
|
||||
setPriority(QThread::IdlePriority);
|
||||
|
||||
forever {
|
||||
m_mutex.lock();
|
||||
|
||||
while (! (m_done || ! m_source.fileName.isEmpty()))
|
||||
m_condition.wait(&m_mutex);
|
||||
|
||||
const bool done = m_done;
|
||||
const Source source = m_source;
|
||||
m_source.clear();
|
||||
|
||||
m_mutex.unlock();
|
||||
|
||||
if (done)
|
||||
break;
|
||||
|
||||
const SemanticInfo info = semanticInfo(source);
|
||||
|
||||
if (! isOutdated()) {
|
||||
m_mutex.lock();
|
||||
m_lastSemanticInfo = info;
|
||||
m_mutex.unlock();
|
||||
|
||||
emit changed(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SemanticInfo SemanticHighlighter::semanticInfo(const Source &source)
|
||||
{
|
||||
m_mutex.lock();
|
||||
const int revision = m_lastSemanticInfo.revision();
|
||||
m_mutex.unlock();
|
||||
|
||||
Snapshot snapshot;
|
||||
Document::Ptr doc;
|
||||
|
||||
if (! source.force && revision == source.revision) {
|
||||
m_mutex.lock();
|
||||
snapshot = m_lastSemanticInfo.snapshot;
|
||||
doc = m_lastSemanticInfo.document;
|
||||
m_mutex.unlock();
|
||||
}
|
||||
|
||||
if (! doc) {
|
||||
snapshot = source.snapshot;
|
||||
doc = snapshot.documentFromSource(source.code, source.fileName);
|
||||
|
||||
// ### This doesn't really work: what if snapshot doesn't have the doc?
|
||||
if (snapshot.document(source.fileName)->qmlProgram())
|
||||
doc->parseQml();
|
||||
else if (snapshot.document(source.fileName)->jsProgram())
|
||||
doc->parseJavaScript();
|
||||
}
|
||||
|
||||
SemanticInfo semanticInfo;
|
||||
semanticInfo.snapshot = snapshot;
|
||||
semanticInfo.document = doc;
|
||||
|
||||
Check checker(doc, snapshot);
|
||||
semanticInfo.semanticMessages = checker();
|
||||
|
||||
return semanticInfo;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,10 @@
|
||||
#include <qmljs/qmljsscanner.h>
|
||||
#include <texteditor/basetexteditor.h>
|
||||
|
||||
#include <QtCore/QWaitCondition>
|
||||
#include <QtCore/QMutex>
|
||||
#include <QtCore/QThread>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QComboBox;
|
||||
class QTimer;
|
||||
@@ -117,6 +121,75 @@ public: // attributes
|
||||
QList<Range> ranges;
|
||||
QHash<QString, QList<QmlJS::AST::SourceLocation> > idLocations;
|
||||
QList<Declaration> declarations;
|
||||
|
||||
// these are in addition to the parser messages in the document
|
||||
QList<QmlJS::DiagnosticMessage> semanticMessages;
|
||||
};
|
||||
|
||||
class SemanticHighlighter: public QThread
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SemanticHighlighter(QObject *parent = 0);
|
||||
virtual ~SemanticHighlighter();
|
||||
|
||||
void abort();
|
||||
|
||||
struct Source
|
||||
{
|
||||
QmlJS::Snapshot snapshot;
|
||||
QString fileName;
|
||||
QString code;
|
||||
int line;
|
||||
int column;
|
||||
int revision;
|
||||
bool force;
|
||||
|
||||
Source()
|
||||
: line(0), column(0), revision(0), force(false)
|
||||
{ }
|
||||
|
||||
Source(const QmlJS::Snapshot &snapshot,
|
||||
const QString &fileName,
|
||||
const QString &code,
|
||||
int line, int column,
|
||||
int revision)
|
||||
: snapshot(snapshot), fileName(fileName),
|
||||
code(code), line(line), column(column),
|
||||
revision(revision), force(false)
|
||||
{ }
|
||||
|
||||
void clear()
|
||||
{
|
||||
snapshot = QmlJS::Snapshot();
|
||||
fileName.clear();
|
||||
code.clear();
|
||||
line = 0;
|
||||
column = 0;
|
||||
revision = 0;
|
||||
force = false;
|
||||
}
|
||||
};
|
||||
|
||||
void rehighlight(const Source &source);
|
||||
|
||||
Q_SIGNALS:
|
||||
void changed(const SemanticInfo &semanticInfo);
|
||||
|
||||
protected:
|
||||
virtual void run();
|
||||
|
||||
private:
|
||||
bool isOutdated();
|
||||
SemanticInfo semanticInfo(const Source &source);
|
||||
|
||||
private:
|
||||
QMutex m_mutex;
|
||||
QWaitCondition m_condition;
|
||||
bool m_done;
|
||||
Source m_source;
|
||||
SemanticInfo m_lastSemanticInfo;
|
||||
};
|
||||
|
||||
class QmlJSTextEditor : public TextEditor::BaseTextEditor
|
||||
@@ -154,6 +227,9 @@ private slots:
|
||||
// refactoring ops
|
||||
void renameIdUnderCursor();
|
||||
|
||||
void semanticRehighlight();
|
||||
void updateSemanticInfo(const SemanticInfo &semanticInfo);
|
||||
|
||||
protected:
|
||||
void contextMenuEvent(QContextMenuEvent *e);
|
||||
TextEditor::BaseTextEditorEditable *createEditableInterface();
|
||||
@@ -173,6 +249,8 @@ private:
|
||||
|
||||
QString wordUnderCursor() const;
|
||||
|
||||
SemanticHighlighter::Source currentSource(bool force = false);
|
||||
|
||||
const Context m_context;
|
||||
|
||||
QTimer *m_updateDocumentTimer;
|
||||
@@ -183,6 +261,7 @@ private:
|
||||
QTextCharFormat m_occurrencesUnusedFormat;
|
||||
QTextCharFormat m_occurrenceRenameFormat;
|
||||
|
||||
SemanticHighlighter *m_semanticHighlighter;
|
||||
SemanticInfo m_semanticInfo;
|
||||
};
|
||||
|
||||
|
||||
@@ -165,7 +165,7 @@ void HoverHandler::updateHelpIdAndTooltip(TextEditor::ITextEditor *editor, int p
|
||||
}
|
||||
|
||||
QString symbolName = QLatin1String("<unknown>");
|
||||
if (m_helpId.isEmpty()) {
|
||||
if (m_helpId.isEmpty() && m_toolTip.isEmpty()) {
|
||||
AST::Node *node = semanticInfo.nodeUnderCursor(pos);
|
||||
if (node && !(AST::cast<AST::StringLiteral *>(node) != 0 || AST::cast<AST::NumericLiteral *>(node) != 0)) {
|
||||
AST::Node *declaringMember = semanticInfo.declaringMember(pos);
|
||||
|
||||
Reference in New Issue
Block a user