Newer
Older
/**************************************************************************
**
** This file is part of Qt Creator
**
** Copyright (c) 2009 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
**
**************************************************************************/
#include "qmljseditor.h"
#include "qmljseditorconstants.h"
#include "qmljseditorplugin.h"
#include "qmlexpressionundercursor.h"
#include "qmllookupcontext.h"
#include "qmlresolveexpression.h"
#include <qscripthighlighter/qscriptindenter.h>
#include <qmljs/qmltypesystem.h>
#include <qmljs/parser/qmljsastvisitor_p.h>
#include <qmljs/parser/qmljsast_p.h>
#include <qmljs/parser/qmljsengine_p.h>
#include <qmljs/qmldocument.h>
#include <qmljs/qmlidcollector.h>
#include <coreplugin/actionmanager/actionmanager.h>
#include <coreplugin/icore.h>
#include <coreplugin/mimedatabase.h>
#include <extensionsystem/pluginmanager.h>
#include <texteditor/basetextdocument.h>
#include <texteditor/fontsettings.h>
#include <texteditor/textblockiterator.h>
#include <texteditor/texteditorconstants.h>
#include <texteditor/texteditorsettings.h>
#include <utils/changeset.h>

Roberto Raggi
committed
#include <utils/uncommentselection.h>
#include <QtCore/QFileInfo>
#include <QtCore/QTimer>
#include <QtGui/QMenu>
#include <QtGui/QComboBox>
UPDATE_DOCUMENT_DEFAULT_INTERVAL = 250,
UPDATE_USES_DEFAULT_INTERVAL = 150
using namespace Qml;
using namespace QmlJS;
using namespace QmlJS::AST;
namespace {
int blockBraceDepth(const QTextBlock &block)
{
int state = block.userState();
if (state == -1)
return 0;
return (state >> 8) & 0xFF;
}
int blockStartState(const QTextBlock &block)
{
int state = block.userState();
if (state == -1)
return 0;
else
return state & 0xff;
}
} // end of anonymous namespace
namespace QmlJSEditor {
typedef QMap<QString, QList<AST::SourceLocation> > Result;
Result operator()(AST::Node *node)
{
_ids.clear();
_maybeIds.clear();
accept(node);
return _ids;
}
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
QString asString(AST::UiQualifiedId *id)
{
QString text;
for (; id; id = id->next) {
if (id->name)
text += id->name->asString();
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;
virtual bool visit(AST::UiScriptBinding *node)
{
if (asString(node->qualifiedId) == QLatin1String("id")) {
if (AST::ExpressionStatement *stmt = AST::cast<AST::ExpressionStatement*>(node->statement)) {
if (AST::IdentifierExpression *idExpr = AST::cast<AST::IdentifierExpression *>(stmt->expression)) {
if (idExpr->name) {
const QString id = idExpr->name->asString();
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;
}
virtual bool visit(AST::IdentifierExpression *node)
{
if (node->name) {
const QString name = node->name->asString();
if (_ids.contains(name))
_ids[name].append(node->identifierToken);
else
_maybeIds[name].append(node->identifierToken);
}
return false;
}
Result _ids;
Result _maybeIds;
class FindDeclarations: protected Visitor
{
QList<Declaration> _declarations;
int _depth;
QList<Declaration> operator()(AST::Node *node)
{
_depth = -1;
_declarations.clear();
accept(node);
return _declarations;
}
using Visitor::visit;
using Visitor::endVisit;
QString asString(AST::UiQualifiedId *id)
{
QString text;
for (; id; id = id->next) {
if (id->name)
text += id->name->asString();
else
text += QLatin1Char('?');
if (id->next)
text += QLatin1Char('.');
}
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;
}
virtual bool visit(AST::UiObjectDefinition *node)
{
++_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
}
virtual void endVisit(AST::UiObjectDefinition *)
{
--_depth;
}
virtual bool visit(AST::UiObjectBinding *node)
{
++_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
}
virtual void endVisit(AST::UiObjectBinding *)
{
--_depth;
}
#if 0 // ### ignore script bindings for now.
virtual bool visit(AST::UiScriptBinding *node)
{
++_depth;
Declaration decl;
init(&decl, node);
decl.text.fill(QLatin1Char(' '), _depth);
decl.text.append(asString(node->qualifiedId));
_declarations.append(decl);
return false; // more more bindings in this subtree.
}
virtual void endVisit(AST::UiScriptBinding *)
{
--_depth;
}
QmlJSEditorEditable::QmlJSEditorEditable(QmlJSTextEditor *editor)
Core::UniqueIDManager *uidm = Core::UniqueIDManager::instance();
m_context << uidm->uniqueIdentifier(QmlJSEditor::Constants::C_QMLJSEDITOR_ID);
m_context << uidm->uniqueIdentifier(TextEditor::Constants::C_TEXTEDITOR);
QmlJSTextEditor::QmlJSTextEditor(QWidget *parent) :
TextEditor::BaseTextEditor(parent),
m_modelManager(0),
m_typeSystem(0)
setParenthesesMatchingEnabled(true);
setMarksVisible(true);
setCodeFoldingSupported(true);
setCodeFoldingVisible(true);
m_updateDocumentTimer = new QTimer(this);
m_updateDocumentTimer->setInterval(UPDATE_DOCUMENT_DEFAULT_INTERVAL);
m_updateDocumentTimer->setSingleShot(true);
connect(m_updateDocumentTimer, SIGNAL(timeout()), this, SLOT(updateDocumentNow()));
m_updateUsesTimer = new QTimer(this);
m_updateUsesTimer->setInterval(UPDATE_USES_DEFAULT_INTERVAL);
m_updateUsesTimer->setSingleShot(true);
connect(m_updateUsesTimer, SIGNAL(timeout()), this, SLOT(updateUsesNow()));
connect(this, SIGNAL(textChanged()), this, SLOT(updateDocument()));
connect(this, SIGNAL(textChanged()), this, SLOT(updateUses()));
baseTextDocument()->setSyntaxHighlighter(new QmlHighlighter);
m_modelManager = ExtensionSystem::PluginManager::instance()->getObject<QmlModelManagerInterface>();
m_typeSystem = ExtensionSystem::PluginManager::instance()->getObject<Qml::QmlTypeSystem>();
connect(m_modelManager, SIGNAL(documentUpdated(Qml::QmlDocument::Ptr)),
this, SLOT(onDocumentUpdated(Qml::QmlDocument::Ptr)));
QmlJSTextEditor::~QmlJSTextEditor()
QList<Declaration> QmlJSTextEditor::declarations() const
Core::IEditor *QmlJSEditorEditable::duplicate(QWidget *parent)
QmlJSTextEditor *newEditor = new QmlJSTextEditor(parent);
newEditor->duplicateFrom(editor());
QmlJSEditorPlugin::instance()->initializeEditor(newEditor);
return newEditor->editableInterface();
QString QmlJSEditorEditable::id() const
return QLatin1String(QmlJSEditor::Constants::C_QMLJSEDITOR_ID);
bool QmlJSEditorEditable::open(const QString &fileName)
{
bool b = TextEditor::BaseTextEditorEditable::open(fileName);
editor()->setMimeType(Core::ICore::instance()->mimeDatabase()->findByFile(QFileInfo(fileName)).type());
return b;
}
QmlJSTextEditor::Context QmlJSEditorEditable::context() const
void QmlJSTextEditor::updateDocument()
m_updateDocumentTimer->start(UPDATE_DOCUMENT_DEFAULT_INTERVAL);
void QmlJSTextEditor::updateDocumentNow()
// ### move in the parser thread.
m_updateDocumentTimer->stop();
const QString fileName = file()->fileName();
m_modelManager->updateSourceFiles(QStringList() << fileName);
}
void QmlJSTextEditor::onDocumentUpdated(Qml::QmlDocument::Ptr doc)
{
if (file()->fileName() != doc->fileName())
return;
m_document = doc;
FindIdDeclarations updateIds;
m_idsRevision = document()->revision();
m_ids = updateIds(doc->program());
FindDeclarations findDeclarations;
m_declarations = findDeclarations(doc->program());
QStringList items;
items.append(tr("<Select Symbol>"));
foreach (Declaration decl, m_declarations)
items.append(decl.text);
m_methodCombo->clear();
m_methodCombo->addItems(items);
updateMethodBoxIndex();
QList<QTextEdit::ExtraSelection> selections;
QTextCharFormat errorFormat;
errorFormat.setUnderlineColor(Qt::red);
errorFormat.setUnderlineStyle(QTextCharFormat::WaveUnderline);
QTextEdit::ExtraSelection sel;
m_diagnosticMessages = doc->diagnosticMessages();
foreach (const DiagnosticMessage &d, m_diagnosticMessages) {
int line = d.loc.startLine;
int column = d.loc.startColumn;
if (column == 0)
column = 1;
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 = errorFormat;
selections.append(sel);
}
setExtraSelections(CodeWarningsSelection, selections);
void QmlJSTextEditor::jumpToMethod(int index)
if (index) {
Declaration d = m_declarations.at(index - 1);
gotoLine(d.startLine, d.startColumn - 1);
setFocus();
}
void QmlJSTextEditor::updateMethodBoxIndex()
int line = 0, column = 0;
convertPosition(position(), &line, &column);
int currentSymbolIndex = 0;
int index = 0;
while (index < m_declarations.size()) {
const Declaration &d = m_declarations.at(index++);
if (line < d.startLine)
break;
else
currentSymbolIndex = index;
}
m_methodCombo->setCurrentIndex(currentSymbolIndex);
void QmlJSTextEditor::updateUses()
{
m_updateUsesTimer->start();
}
void QmlJSTextEditor::updateUsesNow()
{
if (document()->revision() != m_idsRevision) {
updateUses();
return;
}
m_updateUsesTimer->stop();
QList<QTextEdit::ExtraSelection> selections;
foreach (const AST::SourceLocation &loc, m_ids.value(wordUnderCursor())) {
if (! loc.isValid())
continue;
QTextEdit::ExtraSelection sel;
sel.cursor = textCursor();
sel.cursor.setPosition(loc.begin());
sel.cursor.setPosition(loc.end(), QTextCursor::KeepAnchor);
selections.append(sel);
}
setExtraSelections(CodeSemanticsSelection, selections);
void QmlJSTextEditor::updateMethodBoxToolTip()
void QmlJSTextEditor::updateFileName()
void QmlJSTextEditor::renameIdUnderCursor()
const QString id = wordUnderCursor();
bool ok = false;
const QString newId = QInputDialog::getText(Core::ICore::instance()->mainWindow(),
tr("Rename..."),
tr("New id:"),
QLineEdit::Normal,
id, &ok);
if (ok) {
Utils::ChangeSet changeSet;
foreach (const AST::SourceLocation &loc, m_ids.value(id)) {
changeSet.replace(loc.offset, loc.length, newId);
}
QTextCursor tc = textCursor();
changeSet.apply(&tc);
QStringList QmlJSTextEditor::keywords() const

Roberto Raggi
committed
{

Roberto Raggi
committed
if (QmlHighlighter *highlighter = qobject_cast<QmlHighlighter*>(baseTextDocument()->syntaxHighlighter()))
words = highlighter->keywords().toList();

Roberto Raggi
committed

Roberto Raggi
committed
}
void QmlJSTextEditor::setFontSettings(const TextEditor::FontSettings &fs)
TextEditor::BaseTextEditor::setFontSettings(fs);
QmlHighlighter *highlighter = qobject_cast<QmlHighlighter*>(baseTextDocument()->syntaxHighlighter());
if (!highlighter)
return;
static QVector<QString> categories;
if (categories.isEmpty()) {
categories << QLatin1String(TextEditor::Constants::C_NUMBER)
<< QLatin1String(TextEditor::Constants::C_STRING)
<< QLatin1String(TextEditor::Constants::C_TYPE)
<< QLatin1String(TextEditor::Constants::C_KEYWORD)
<< QLatin1String(TextEditor::Constants::C_PREPROCESSOR)
<< QLatin1String(TextEditor::Constants::C_LABEL)
<< QLatin1String(TextEditor::Constants::C_COMMENT)
<< QLatin1String(TextEditor::Constants::C_VISUAL_WHITESPACE);
m_occurrencesFormat = fs.toTextCharFormat(QLatin1String(TextEditor::Constants::C_OCCURRENCES));
m_occurrencesFormat.clearForeground();
highlighter->setFormats(fs.toTextCharFormats(categories));
highlighter->rehighlight();
QString QmlJSTextEditor::wordUnderCursor() const
QTextCursor tc = textCursor();
tc.movePosition(QTextCursor::StartOfWord);
tc.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
const QString word= tc.selectedText();
return word;
bool QmlJSTextEditor::isElectricCharacter(const QChar &ch) const
if (ch == QLatin1Char('}')
|| ch == QLatin1Char(']'))
return true;
return false;
bool QmlJSTextEditor::isClosingBrace(const QList<QScriptIncrementalScanner::Token> &tokens) const
if (tokens.size() == 1) {
const QScriptIncrementalScanner::Token firstToken = tokens.first();
return firstToken.is(QScriptIncrementalScanner::Token::RightBrace) || firstToken.is(QScriptIncrementalScanner::Token::RightBracket);
}
void QmlJSTextEditor::indentBlock(QTextDocument *doc, QTextBlock block, QChar typedChar)
{
TextEditor::TabSettings ts = tabSettings();
SharedTools::QScriptIndenter indenter;
indenter.setTabSize(ts.m_tabSize);
indenter.setIndentSize(ts.m_indentSize);
const int indent = indenter.indentForBottomLine(doc->begin(), block.next(), typedChar);
TextEditor::BaseTextEditorEditable *QmlJSTextEditor::createEditableInterface()
QmlJSEditorEditable *editable = new QmlJSEditorEditable(this);
createToolBar(editable);
return editable;
void QmlJSTextEditor::createToolBar(QmlJSEditorEditable *editable)
m_methodCombo = new QComboBox;
m_methodCombo->setMinimumContentsLength(22);
//m_methodCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
// Make the combo box prefer to expand
QSizePolicy policy = m_methodCombo->sizePolicy();
policy.setHorizontalPolicy(QSizePolicy::Expanding);
m_methodCombo->setSizePolicy(policy);
connect(m_methodCombo, SIGNAL(activated(int)), this, SLOT(jumpToMethod(int)));
connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(updateMethodBoxIndex()));
connect(m_methodCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(updateMethodBoxToolTip()));
connect(file(), SIGNAL(changed()), this, SLOT(updateFileName()));
QToolBar *toolBar = static_cast<QToolBar*>(editable->toolBar());
QList<QAction*> actions = toolBar->actions();
toolBar->insertWidget(actions.first(), m_methodCombo);
TextEditor::BaseTextEditor::Link QmlJSTextEditor::findLinkAt(const QTextCursor &cursor, bool /*resolveTarget*/)
{
Link link;
if (!m_modelManager)
return link;
const Snapshot snapshot = m_modelManager->snapshot();
QmlDocument::Ptr doc = snapshot.document(file()->fileName());
QTextCursor expressionCursor(cursor);
{
// correct the position by moving to the end of an identifier (if we're hovering over one):
int pos = cursor.position();
forever {
const QChar ch = characterAt(pos);
if (ch.isLetterOrNumber() || ch == QLatin1Char('_'))
++pos;
else
break;
}
expressionCursor.setPosition(pos);
}
QmlExpressionUnderCursor expressionUnderCursor;
expressionUnderCursor(expressionCursor, doc);
QmlLookupContext context(expressionUnderCursor.expressionScopes(), doc, snapshot, m_typeSystem);
QmlResolveExpression resolver(context);
QmlSymbol *symbol = resolver.typeOf(expressionUnderCursor.expressionNode());
if (!symbol)
return link;
if (const QmlSymbolFromFile *target = symbol->asSymbolFromFile()) {
link.pos = expressionUnderCursor.expressionOffset();
link.length = expressionUnderCursor.expressionLength();
link.fileName = target->fileName();
link.line = target->line();
link.column = target->column();
if (link.column > 0)
--link.column;
}
void QmlJSTextEditor::contextMenuEvent(QContextMenuEvent *e)
QMenu *menu = new QMenu();
if (Core::ActionContainer *mcontext = Core::ICore::instance()->actionManager()->actionContainer(QmlJSEditor::Constants::M_CONTEXT)) {
QMenu *contextMenu = mcontext->menu();
foreach (QAction *action, contextMenu->actions())
menu->addAction(action);
}
const QString id = wordUnderCursor();
const QList<AST::SourceLocation> &locations = m_ids.value(id);
if (! locations.isEmpty()) {
menu->addSeparator();
QAction *a = menu->addAction(tr("Rename id '%1'...").arg(id));
connect(a, SIGNAL(triggered()), this, SLOT(renameIdUnderCursor()));
}
appendStandardContextMenuActions(menu);
menu->exec(e->globalPos());
menu->deleteLater();
void QmlJSTextEditor::unCommentSelection()

Roberto Raggi
committed
{
Utils::unCommentSelection(this);

Roberto Raggi
committed
}
static bool isCompleteStringLiteral(const QStringRef &text)
{
if (text.length() < 2)
return false;
const QChar quote = text.at(0);
if (text.at(text.length() - 1) == quote)
return text.at(text.length() - 2) != QLatin1Char('\\'); // ### not exactly.
return false;
}
bool QmlJSTextEditor::contextAllowsAutoParentheses(const QTextCursor &cursor, const QString &textToInsert) const
QChar ch;
if (! textToInsert.isEmpty())
ch = textToInsert.at(0);
switch (ch.unicode()) {
case '\'':
case '"':
case '(':
case '[':
case '{':
case ')':
case ']':
case '}':
case ';':
break;
default:
if (ch.isNull())
break;
return false;
} // end of switch

Roberto Raggi
committed
const QString blockText = cursor.block().text();
const int blockState = blockStartState(cursor.block());
QScriptIncrementalScanner tokenize;
const QList<QScriptIncrementalScanner::Token> tokens = tokenize(blockText, blockState);
const int pos = cursor.columnNumber();
int tokenIndex = 0;
for (; tokenIndex < tokens.size(); ++tokenIndex) {

Roberto Raggi
committed
const QScriptIncrementalScanner::Token &token = tokens.at(tokenIndex);
if (pos >= token.begin()) {
if (pos < token.end())
break;
else if (pos == token.end() && (token.is(QScriptIncrementalScanner::Token::Comment) ||
token.is(QScriptIncrementalScanner::Token::String)))

Roberto Raggi
committed
}
if (tokenIndex != tokens.size()) {

Roberto Raggi
committed
const QScriptIncrementalScanner::Token &token = tokens.at(tokenIndex);
switch (token.kind) {
case QScriptIncrementalScanner::Token::Comment:
return false;
case QScriptIncrementalScanner::Token::String: {
const QStringRef tokenText = blockText.midRef(token.offset, token.length);
const QChar quote = tokenText.at(0);
if (ch == quote && isCompleteStringLiteral(tokenText))
break;

Roberto Raggi
committed
default:
break;
} // end of switch
}
bool QmlJSTextEditor::isInComment(const QTextCursor &) const
{
// ### implement me
return false;
}
QString QmlJSTextEditor::insertMatchingBrace(const QTextCursor &tc, const QString &text, const QChar &, int *skippedChars) const
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
{
if (text.length() != 1)
return QString();
const QChar la = characterAt(tc.position());
const QChar ch = text.at(0);
switch (ch.unicode()) {
case '\'':
if (la != ch)
return QString(ch);
++*skippedChars;
break;
case '"':
if (la != ch)
return QString(ch);
++*skippedChars;
break;
case '(':
return QString(QLatin1Char(')'));
case '[':
return QString(QLatin1Char(']'));
case '{':
return QString(); // nothing to do.
case ')':
case ']':
case '}':
case ';':
if (la == ch)
++*skippedChars;
break;
default:
break;
} // end of switch
return QString();
}
QString QmlJSTextEditor::insertParagraphSeparator(const QTextCursor &) const
{
return QLatin1String("}\n");
}
} // namespace QmlJSEditor