1 /**************************************************************************
3 ** This file is part of Qt Creator
5 ** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies).
7 ** Contact: Nokia Corporation (info@qt.nokia.com)
10 ** GNU Lesser General Public License Usage
12 ** This file may be used under the terms of the GNU Lesser General Public
13 ** License version 2.1 as published by the Free Software Foundation and
14 ** appearing in the file LICENSE.LGPL included in the packaging of this file.
15 ** Please review the following information to ensure the GNU Lesser General
16 ** Public License version 2.1 requirements will be met:
17 ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
19 ** In addition, as a special exception, Nokia gives you certain additional
20 ** rights. These rights are described in the Nokia Qt LGPL Exception
21 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
25 ** Alternatively, this file may be used in accordance with the terms and
26 ** conditions contained in a signed written agreement between you and Nokia.
28 ** If you have questions regarding the use of this file, please contact
29 ** Nokia at qt-info@nokia.com.
31 **************************************************************************/
33 #include "qmljscodecompletion.h"
34 #include "qmlexpressionundercursor.h"
35 #include "qmljseditor.h"
36 #include "qmljseditorconstants.h"
38 #include <qmljs/qmljsmodelmanagerinterface.h>
39 #include <qmljs/parser/qmljsast_p.h>
40 #include <qmljs/qmljsinterpreter.h>
41 #include <qmljs/qmljslookupcontext.h>
42 #include <qmljs/qmljsscanner.h>
43 #include <qmljs/qmljsbind.h>
44 #include <qmljs/qmljscompletioncontextfinder.h>
45 #include <qmljs/qmljsscopebuilder.h>
47 #include <texteditor/basetexteditor.h>
48 #include <texteditor/completionsettings.h>
50 #include <coreplugin/icore.h>
51 #include <coreplugin/editormanager/editormanager.h>
53 #include <utils/faketooltip.h>
54 #include <utils/qtcassert.h>
56 #include <QtCore/QFile>
57 #include <QtCore/QFileInfo>
58 #include <QtCore/QDir>
59 #include <QtCore/QDebug>
60 #include <QtCore/QDirIterator>
62 #include <QtGui/QApplication>
63 #include <QtGui/QDesktopWidget>
64 #include <QtGui/QHBoxLayout>
65 #include <QtGui/QLabel>
66 #include <QtGui/QPainter>
67 #include <QtGui/QStyle>
68 #include <QtGui/QTextBlock>
69 #include <QtGui/QToolButton>
71 using namespace QmlJSEditor;
72 using namespace QmlJSEditor::Internal;
73 using namespace QmlJS;
77 enum CompletionOrder {
86 // Temporary workaround until we have proper icons for QML completion items
87 static QIcon iconForColor(const QColor &color)
94 QPixmap pm(2 * pixSize, 2 * pixSize);
96 pmp.fillRect(0, 0, pixSize, pixSize, Qt::lightGray);
97 pmp.fillRect(pixSize, pixSize, pixSize, pixSize, Qt::lightGray);
98 pmp.fillRect(0, pixSize, pixSize, pixSize, Qt::darkGray);
99 pmp.fillRect(pixSize, 0, pixSize, pixSize, Qt::darkGray);
100 pmp.fillRect(0, 0, 2 * pixSize, 2 * pixSize, color);
105 QRect r = pix.rect().adjusted(corr, corr, -corr, -corr);
106 p.setBrushOrigin((r.width() % pixSize + pixSize) / 2 + corr, (r.height() % pixSize + pixSize) / 2 + corr);
109 p.fillRect(r.width() / 4 + corr, r.height() / 4 + corr,
110 r.width() / 2, r.height() / 2,
111 QColor(color.rgb()));
112 p.drawRect(pix.rect().adjusted(0, 0, -1, -1));
117 static bool checkStartOfIdentifier(const QString &word)
122 const QChar ch = word.at(0);
124 switch (ch.unicode()) {
129 return ch.isLetter();
133 static bool isIdentifierChar(QChar ch)
135 switch (ch.unicode()) {
140 return ch.isLetterOrNumber();
144 class SearchPropertyDefinitions: protected AST::Visitor
146 QList<AST::UiPublicMember *> _properties;
149 QList<AST::UiPublicMember *> operator()(Document::Ptr doc)
152 if (doc && doc->qmlProgram())
153 doc->qmlProgram()->accept(this);
159 using AST::Visitor::visit;
161 virtual bool visit(AST::UiPublicMember *member)
163 if (member->propertyToken.isValid()) {
164 _properties.append(member);
171 class EnumerateProperties: private Interpreter::MemberProcessor
173 QSet<const Interpreter::ObjectValue *> _processed;
174 QHash<QString, const Interpreter::Value *> _properties;
175 bool _globalCompletion;
176 bool _enumerateGeneratedSlots;
177 const Interpreter::Context *_context;
178 const Interpreter::ObjectValue *_currentObject;
181 EnumerateProperties(const Interpreter::Context *context)
182 : _globalCompletion(false),
183 _enumerateGeneratedSlots(false),
189 void setGlobalCompletion(bool globalCompletion)
191 _globalCompletion = globalCompletion;
194 void setEnumerateGeneratedSlots(bool enumerate)
196 _enumerateGeneratedSlots = enumerate;
199 QHash<QString, const Interpreter::Value *> operator ()(const Interpreter::Value *value)
203 _currentObject = Interpreter::value_cast<const Interpreter::ObjectValue *>(value);
205 enumerateProperties(value);
210 QHash<QString, const Interpreter::Value *> operator ()()
216 foreach (const Interpreter::ObjectValue *scope, _context->scopeChain().all())
217 enumerateProperties(scope);
223 void insertProperty(const QString &name, const Interpreter::Value *value)
225 _properties.insert(name, value);
228 virtual bool processProperty(const QString &name, const Interpreter::Value *value)
230 insertProperty(name, value);
234 virtual bool processEnumerator(const QString &name, const Interpreter::Value *value)
236 if (! _globalCompletion)
237 insertProperty(name, value);
241 virtual bool processSignal(const QString &, const Interpreter::Value *)
246 virtual bool processSlot(const QString &name, const Interpreter::Value *value)
248 insertProperty(name, value);
252 virtual bool processGeneratedSlot(const QString &name, const Interpreter::Value *value)
254 if (_enumerateGeneratedSlots || (_currentObject && _currentObject->className().endsWith(QLatin1String("Keys")))) {
255 // ### FIXME: add support for attached properties.
256 insertProperty(name, value);
261 void enumerateProperties(const Interpreter::Value *value)
265 else if (const Interpreter::ObjectValue *object = value->asObjectValue()) {
266 enumerateProperties(object);
270 void enumerateProperties(const Interpreter::ObjectValue *object)
272 if (! object || _processed.contains(object))
275 _processed.insert(object);
276 enumerateProperties(object->prototype(_context));
278 object->processMembers(this);
282 } // end of anonymous namespace
284 namespace QmlJSEditor {
287 class FunctionArgumentWidget : public QLabel
290 FunctionArgumentWidget();
291 void showFunctionHint(const QString &functionName,
292 const QStringList &signature,
296 bool eventFilter(QObject *obj, QEvent *e);
299 void updateArgumentHighlight();
300 void updateHintText();
302 QString m_functionName;
303 QStringList m_signature;
304 int m_minimumArgumentCount;
308 bool m_escapePressed;
310 TextEditor::ITextEditor *m_editor;
313 QLabel *m_numberLabel;
314 Utils::FakeToolTip *m_popupFrame;
318 FunctionArgumentWidget::FunctionArgumentWidget():
319 m_minimumArgumentCount(0),
322 m_escapePressed(false)
324 QObject *editorObject = Core::EditorManager::instance()->currentEditor();
325 m_editor = qobject_cast<TextEditor::ITextEditor *>(editorObject);
327 m_popupFrame = new Utils::FakeToolTip(m_editor->widget());
329 setParent(m_popupFrame);
330 setFocusPolicy(Qt::NoFocus);
332 m_pager = new QWidget;
333 QHBoxLayout *hbox = new QHBoxLayout(m_pager);
336 m_numberLabel = new QLabel;
337 hbox->addWidget(m_numberLabel);
339 QHBoxLayout *layout = new QHBoxLayout;
340 layout->setMargin(0);
341 layout->setSpacing(0);
342 layout->addWidget(m_pager);
343 layout->addWidget(this);
344 m_popupFrame->setLayout(layout);
346 setTextFormat(Qt::RichText);
348 qApp->installEventFilter(this);
351 void FunctionArgumentWidget::showFunctionHint(const QString &functionName, const QStringList &signature, int startPosition)
353 if (m_startpos == startPosition)
356 m_functionName = functionName;
357 m_signature = signature;
358 m_minimumArgumentCount = signature.size();
359 m_startpos = startPosition;
361 m_escapePressed = false;
365 updateArgumentHighlight();
367 m_popupFrame->show();
370 void FunctionArgumentWidget::updateArgumentHighlight()
372 int curpos = m_editor->position();
373 if (curpos < m_startpos) {
374 m_popupFrame->close();
380 QString str = m_editor->textAt(m_startpos, curpos - m_startpos);
384 const QList<Token> tokens = tokenize(str);
385 for (int i = 0; i < tokens.count(); ++i) {
386 const Token &tk = tokens.at(i);
387 if (tk.is(Token::LeftParenthesis))
389 else if (tk.is(Token::RightParenthesis))
391 else if (! parcount && tk.is(Token::Colon))
395 if (m_currentarg != argnr) {
396 // m_currentarg = argnr;
401 m_popupFrame->close();
404 bool FunctionArgumentWidget::eventFilter(QObject *obj, QEvent *e)
407 case QEvent::ShortcutOverride:
408 if (static_cast<QKeyEvent*>(e)->key() == Qt::Key_Escape) {
409 m_escapePressed = true;
412 case QEvent::KeyPress:
413 if (static_cast<QKeyEvent*>(e)->key() == Qt::Key_Escape) {
414 m_escapePressed = true;
417 case QEvent::KeyRelease:
418 if (static_cast<QKeyEvent*>(e)->key() == Qt::Key_Escape && m_escapePressed) {
419 m_popupFrame->close();
422 updateArgumentHighlight();
424 case QEvent::WindowDeactivate:
425 case QEvent::FocusOut:
426 if (obj != m_editor->widget())
428 m_popupFrame->close();
430 case QEvent::MouseButtonPress:
431 case QEvent::MouseButtonRelease:
432 case QEvent::MouseButtonDblClick:
433 case QEvent::Wheel: {
434 QWidget *widget = qobject_cast<QWidget *>(obj);
435 if (! (widget == this || m_popupFrame->isAncestorOf(widget))) {
436 m_popupFrame->close();
446 void FunctionArgumentWidget::updateHintText()
448 QString prettyMethod;
449 prettyMethod += QString::fromLatin1("function ");
450 prettyMethod += m_functionName;
451 prettyMethod += QLatin1Char('(');
452 for (int i = 0; i < m_minimumArgumentCount; ++i) {
454 prettyMethod += QLatin1String(", ");
456 QString arg = m_signature.at(i);
458 arg = QLatin1String("arg");
459 arg += QString::number(i + 1);
464 prettyMethod += QLatin1Char(')');
466 m_numberLabel->setText(prettyMethod);
468 m_popupFrame->setFixedWidth(m_popupFrame->minimumSizeHint().width());
470 const QDesktopWidget *desktop = QApplication::desktop();
472 const QRect screen = desktop->availableGeometry(desktop->screenNumber(m_editor->widget()));
474 const QRect screen = desktop->screenGeometry(desktop->screenNumber(m_editor->widget()));
477 const QSize sz = m_popupFrame->sizeHint();
478 QPoint pos = m_editor->cursorRect(m_startpos).topLeft();
479 pos.setY(pos.y() - sz.height() - 1);
481 if (pos.x() + sz.width() > screen.right())
482 pos.setX(screen.right() - sz.width());
484 m_popupFrame->move(pos);
487 } } // namespace QmlJSEditor::Internal
489 CodeCompletion::CodeCompletion(ModelManagerInterface *modelManager, QObject *parent)
490 : TextEditor::ICompletionCollector(parent),
491 m_modelManager(modelManager),
494 m_restartCompletion(false),
495 m_snippetProvider(Constants::QML_SNIPPETS_GROUP_ID, iconForColor(Qt::red), SnippetOrder)
497 Q_ASSERT(modelManager);
500 CodeCompletion::~CodeCompletion()
503 TextEditor::ITextEditor *CodeCompletion::editor() const
506 int CodeCompletion::startPosition() const
507 { return m_startPosition; }
509 bool CodeCompletion::shouldRestartCompletion()
510 { return m_restartCompletion; }
512 bool CodeCompletion::supportsEditor(TextEditor::ITextEditor *editor) const
514 if (qobject_cast<QmlJSTextEditorWidget *>(editor->widget()))
520 bool CodeCompletion::supportsPolicy(TextEditor::CompletionPolicy policy) const
522 return policy == TextEditor::SemanticCompletion;
525 bool CodeCompletion::triggersCompletion(TextEditor::ITextEditor *editor)
527 if (maybeTriggersCompletion(editor)) {
528 // check the token under cursor
530 if (QmlJSTextEditorWidget *ed = qobject_cast<QmlJSTextEditorWidget *>(editor->widget())) {
532 QTextCursor tc = ed->textCursor();
533 QTextBlock block = tc.block();
534 const int column = tc.positionInBlock();
535 const QChar ch = block.text().at(column - 1);
536 const int blockState = qMax(0, block.previous().userState()) & 0xff;
537 const QString blockText = block.text();
540 const QList<Token> tokens = scanner(blockText, blockState);
541 foreach (const Token &tk, tokens) {
542 if (column >= tk.begin() && column <= tk.end()) {
543 if (ch == QLatin1Char('/') && tk.is(Token::String))
544 return true; // path completion inside string literals
545 if (tk.is(Token::Comment) || tk.is(Token::String))
550 if (ch == QLatin1Char('/'))
559 bool CodeCompletion::maybeTriggersCompletion(TextEditor::ITextEditor *editor)
561 const int cursorPosition = editor->position();
562 const QChar ch = editor->characterAt(cursorPosition - 1);
564 if (ch == QLatin1Char('(') || ch == QLatin1Char('.') || ch == QLatin1Char('/'))
566 if (completionSettings().m_completionTrigger != TextEditor::AutomaticCompletion)
569 const QChar characterUnderCursor = editor->characterAt(cursorPosition);
571 if (isIdentifierChar(ch) && (characterUnderCursor.isSpace() ||
572 characterUnderCursor.isNull() ||
573 isDelimiter(characterUnderCursor))) {
574 int pos = editor->position() - 1;
575 for (; pos != -1; --pos) {
576 if (! isIdentifierChar(editor->characterAt(pos)))
581 const QString word = editor->textAt(pos, cursorPosition - pos);
582 if (word.length() > 2 && checkStartOfIdentifier(word)) {
583 for (int i = 0; i < word.length(); ++i) {
584 if (! isIdentifierChar(word.at(i)))
594 bool CodeCompletion::isDelimiter(QChar ch) const
596 switch (ch.unicode()) {
618 static bool isLiteral(AST::Node *ast)
620 if (AST::cast<AST::StringLiteral *>(ast))
622 else if (AST::cast<AST::NumericLiteral *>(ast))
628 bool CodeCompletion::completeUrl(const QString &relativeBasePath, const QString &urlString)
630 const QUrl url(urlString);
631 QString fileName = url.toLocalFile();
632 if (fileName.isEmpty())
635 return completeFileName(relativeBasePath, fileName);
638 bool CodeCompletion::completeFileName(const QString &relativeBasePath, const QString &fileName,
639 const QStringList &patterns)
641 const QFileInfo fileInfo(fileName);
642 QString directoryPrefix;
643 if (fileInfo.isRelative()) {
644 directoryPrefix = relativeBasePath;
645 directoryPrefix += QDir::separator();
646 directoryPrefix += fileInfo.path();
648 directoryPrefix = fileInfo.path();
650 if (!QFileInfo(directoryPrefix).exists())
653 QDirIterator dirIterator(directoryPrefix, patterns, QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot);
654 while (dirIterator.hasNext()) {
656 const QString fileName = dirIterator.fileName();
658 TextEditor::CompletionItem item(this);
659 item.text += fileName;
660 // ### Icon for file completions
661 item.icon = iconForColor(Qt::darkBlue);
662 m_completions.append(item);
665 return !m_completions.isEmpty();
668 void CodeCompletion::addCompletions(const QHash<QString, const Interpreter::Value *> &newCompletions,
669 const QIcon &icon, int order)
671 QHashIterator<QString, const Interpreter::Value *> it(newCompletions);
672 while (it.hasNext()) {
675 TextEditor::CompletionItem item(this);
676 item.text = it.key();
679 m_completions.append(item);
683 void CodeCompletion::addCompletions(const QStringList &newCompletions,
684 const QIcon &icon, int order)
686 foreach (const QString &text, newCompletions) {
687 TextEditor::CompletionItem item(this);
691 m_completions.append(item);
695 void CodeCompletion::addCompletionsPropertyLhs(
696 const QHash<QString, const Interpreter::Value *> &newCompletions,
697 const QIcon &icon, int order, bool afterOn)
699 QHashIterator<QString, const Interpreter::Value *> it(newCompletions);
700 while (it.hasNext()) {
703 TextEditor::CompletionItem item(this);
704 item.text = it.key();
706 QLatin1String postfix(": ");
708 postfix = QLatin1String(" {");
709 if (const Interpreter::QmlObjectValue *qmlValue = dynamic_cast<const Interpreter::QmlObjectValue *>(it.value())) {
710 // to distinguish "anchors." from "gradient:" we check if the right hand side
711 // type is instantiatable or is the prototype of an instantiatable object
712 if (qmlValue->hasChildInPackage())
713 item.text.append(postfix);
715 item.text.append(QLatin1Char('.'));
717 item.text.append(postfix);
721 m_completions.append(item);
725 static const Interpreter::Value *getPropertyValue(
726 const Interpreter::ObjectValue *object,
727 const QStringList &propertyNames,
728 const Interpreter::Context *context)
730 if (propertyNames.isEmpty() || !object)
733 const Interpreter::Value *value = object;
734 foreach (const QString &name, propertyNames) {
735 if (const Interpreter::ObjectValue *objectValue = value->asObjectValue()) {
736 value = objectValue->property(name, context);
746 int CodeCompletion::startCompletion(TextEditor::ITextEditor *editor)
748 m_restartCompletion = false;
752 QmlJSTextEditorWidget *edit = qobject_cast<QmlJSTextEditorWidget *>(m_editor->widget());
756 m_startPosition = editor->position();
757 const QString fileName = editor->file()->fileName();
759 while (editor->characterAt(m_startPosition - 1).isLetterOrNumber() ||
760 editor->characterAt(m_startPosition - 1) == QLatin1Char('_'))
763 m_completions.clear();
765 const SemanticInfo semanticInfo = edit->semanticInfo();
767 if (! semanticInfo.isValid())
770 const Document::Ptr document = semanticInfo.document;
771 const QFileInfo currentFileInfo(fileName);
773 bool isQmlFile = false;
774 if (currentFileInfo.suffix() == QLatin1String("qml"))
777 const QIcon symbolIcon = iconForColor(Qt::darkCyan);
778 const QIcon keywordIcon = iconForColor(Qt::darkYellow);
780 const QList<AST::Node *> path = semanticInfo.astPath(editor->position());
781 LookupContext::Ptr lookupContext = semanticInfo.lookupContext(path);
782 const Interpreter::Context *context = lookupContext->context();
784 // Search for the operator that triggered the completion.
785 QChar completionOperator;
786 if (m_startPosition > 0)
787 completionOperator = editor->characterAt(m_startPosition - 1);
789 QTextCursor startPositionCursor(edit->document());
790 startPositionCursor.setPosition(m_startPosition);
791 CompletionContextFinder contextFinder(startPositionCursor);
793 const Interpreter::ObjectValue *qmlScopeType = 0;
794 if (contextFinder.isInQmlContext()) {
795 // ### this should use semanticInfo.declaringMember instead, but that may also return functions
796 for (int i = path.size() - 1; i >= 0; --i) {
797 AST::Node *node = path[i];
798 if (AST::cast<AST::UiObjectDefinition *>(node) || AST::cast<AST::UiObjectBinding *>(node)) {
799 qmlScopeType = document->bind()->findQmlObject(node);
804 // fallback to getting the base type object
806 qmlScopeType = context->lookupType(document.data(), contextFinder.qmlObjectTypeName());
809 if (contextFinder.isInStringLiteral()) {
810 // get the text of the literal up to the cursor position
811 QTextCursor tc = edit->textCursor();
812 QmlExpressionUnderCursor expressionUnderCursor;
813 expressionUnderCursor(tc);
814 QString literalText = expressionUnderCursor.text();
815 QTC_ASSERT(!literalText.isEmpty() && (
816 literalText.at(0) == QLatin1Char('"')
817 || literalText.at(0) == QLatin1Char('\'')), return -1);
818 literalText = literalText.mid(1);
820 if (contextFinder.isInImport()) {
821 QStringList patterns;
822 patterns << QLatin1String("*.qml") << QLatin1String("*.js");
823 if (completeFileName(document->path(), literalText, patterns))
824 return m_startPosition;
828 const Interpreter::Value *value = getPropertyValue(qmlScopeType, contextFinder.bindingPropertyName(), context);
831 } else if (value->asUrlValue()) {
832 if (completeUrl(document->path(), literalText))
833 return m_startPosition;
836 // ### enum completion?
838 // completion gets triggered for / in string literals, if we don't
839 // return here, this will mean the snippet completion pops up for
840 // each / in a string literal that is not triggering file completion
842 } else if (completionOperator.isSpace() || completionOperator.isNull() || isDelimiter(completionOperator) ||
843 (completionOperator == QLatin1Char('(') && m_startPosition != editor->position())) {
845 bool doGlobalCompletion = true;
846 bool doQmlKeywordCompletion = true;
847 bool doJsKeywordCompletion = true;
848 bool doQmlTypeCompletion = false;
850 if (contextFinder.isInLhsOfBinding() && qmlScopeType) {
851 doGlobalCompletion = false;
852 doJsKeywordCompletion = false;
853 doQmlTypeCompletion = true;
855 EnumerateProperties enumerateProperties(context);
856 enumerateProperties.setGlobalCompletion(true);
857 enumerateProperties.setEnumerateGeneratedSlots(true);
860 TextEditor::CompletionItem idPropertyCompletion(this);
861 idPropertyCompletion.text = QLatin1String("id: ");
862 idPropertyCompletion.icon = symbolIcon;
863 idPropertyCompletion.order = PropertyOrder;
864 m_completions.append(idPropertyCompletion);
866 addCompletionsPropertyLhs(enumerateProperties(qmlScopeType), symbolIcon, PropertyOrder, contextFinder.isAfterOnInLhsOfBinding());
868 if (ScopeBuilder::isPropertyChangesObject(context, qmlScopeType)
869 && context->scopeChain().qmlScopeObjects.size() == 2) {
870 addCompletions(enumerateProperties(context->scopeChain().qmlScopeObjects.first()), symbolIcon, SymbolOrder);
874 if (contextFinder.isInRhsOfBinding() && qmlScopeType) {
875 doQmlKeywordCompletion = false;
877 // complete enum values for enum properties
878 const Interpreter::Value *value = getPropertyValue(qmlScopeType, contextFinder.bindingPropertyName(), context);
879 if (const Interpreter::QmlEnumValue *enumValue = dynamic_cast<const Interpreter::QmlEnumValue *>(value)) {
880 foreach (const QString &key, enumValue->keys()) {
881 TextEditor::CompletionItem item(this);
883 item.data = QString("\"%1\"").arg(key);
884 item.icon = symbolIcon;
885 item.order = EnumValueOrder;
886 m_completions.append(item);
891 if (!contextFinder.isInImport() && !contextFinder.isInQmlContext())
892 doQmlTypeCompletion = true;
894 if (doQmlTypeCompletion) {
895 if (const Interpreter::ObjectValue *qmlTypes = context->scopeChain().qmlTypes) {
896 EnumerateProperties enumerateProperties(context);
897 addCompletions(enumerateProperties(qmlTypes), symbolIcon, TypeOrder);
901 if (doGlobalCompletion) {
902 // It's a global completion.
903 EnumerateProperties enumerateProperties(context);
904 enumerateProperties.setGlobalCompletion(true);
905 addCompletions(enumerateProperties(), symbolIcon, SymbolOrder);
908 if (doJsKeywordCompletion) {
910 addCompletions(Scanner::keywords(), keywordIcon, KeywordOrder);
913 // add qml extra words
914 if (doQmlKeywordCompletion && isQmlFile) {
915 static QStringList qmlWords;
916 static QStringList qmlWordsAlsoInJs;
918 if (qmlWords.isEmpty()) {
919 qmlWords << QLatin1String("property")
920 //<< QLatin1String("readonly")
921 << QLatin1String("signal")
922 << QLatin1String("import");
924 if (qmlWordsAlsoInJs.isEmpty()) {
925 qmlWordsAlsoInJs << QLatin1String("default")
926 << QLatin1String("function");
929 addCompletions(qmlWords, keywordIcon, KeywordOrder);
930 if (!doJsKeywordCompletion)
931 addCompletions(qmlWordsAlsoInJs, keywordIcon, KeywordOrder);
935 else if (completionOperator == QLatin1Char('.') || completionOperator == QLatin1Char('(')) {
936 // Look at the expression under cursor.
937 QTextCursor tc = edit->textCursor();
938 tc.setPosition(m_startPosition - 1);
940 QmlExpressionUnderCursor expressionUnderCursor;
941 QmlJS::AST::ExpressionNode *expression = expressionUnderCursor(tc);
943 if (expression != 0 && ! isLiteral(expression)) {
944 // Evaluate the expression under cursor.
945 Interpreter::Engine *interp = lookupContext->engine();
946 const Interpreter::Value *value = interp->convertToObject(lookupContext->evaluate(expression));
947 //qDebug() << "type:" << interp.typeId(value);
949 if (value && completionOperator == QLatin1Char('.')) { // member completion
950 EnumerateProperties enumerateProperties(context);
951 if (contextFinder.isInLhsOfBinding() && qmlScopeType) {
952 enumerateProperties.setEnumerateGeneratedSlots(true);
953 addCompletionsPropertyLhs(enumerateProperties(value), symbolIcon, PropertyOrder, contextFinder.isAfterOnInLhsOfBinding());
955 addCompletions(enumerateProperties(value), symbolIcon, SymbolOrder);
956 } else if (value && completionOperator == QLatin1Char('(') && m_startPosition == editor->position()) {
957 // function completion
958 if (const Interpreter::FunctionValue *f = value->asFunctionValue()) {
959 QString functionName = expressionUnderCursor.text();
960 int indexOfDot = functionName.lastIndexOf(QLatin1Char('.'));
961 if (indexOfDot != -1)
962 functionName = functionName.mid(indexOfDot + 1);
964 // Recreate if necessary
965 if (!m_functionArgumentWidget)
966 m_functionArgumentWidget = new QmlJSEditor::Internal::FunctionArgumentWidget;
968 QStringList signature;
969 for (int i = 0; i < f->argumentCount(); ++i)
970 signature.append(f->argumentName(i));
972 m_functionArgumentWidget->showFunctionHint(functionName.trimmed(),
977 return -1; // We always return -1 when completing function prototypes.
981 if (! m_completions.isEmpty())
982 return m_startPosition;
987 if (isQmlFile && (completionOperator.isNull() || completionOperator.isSpace() || isDelimiter(completionOperator))) {
988 m_completions.append(m_snippetProvider.getSnippets(this));
991 if (! m_completions.isEmpty())
992 return m_startPosition;
997 void CodeCompletion::completions(QList<TextEditor::CompletionItem> *completions)
999 const int length = m_editor->position() - m_startPosition;
1002 *completions = m_completions;
1003 else if (length > 0) {
1004 const QString key = m_editor->textAt(m_startPosition, length);
1006 filter(m_completions, completions, key);
1008 if (completions->size() == 1) {
1009 if (key == completions->first().text)
1010 completions->clear();
1015 bool CodeCompletion::typedCharCompletes(const TextEditor::CompletionItem &item, QChar typedChar)
1017 if (item.data.canConvert<QString>()) // snippet
1020 return (item.text.endsWith(QLatin1String(": ")) && typedChar == QLatin1Char(':'))
1021 || (item.text.endsWith(QLatin1Char('.')) && typedChar == QLatin1Char('.'));
1024 void CodeCompletion::complete(const TextEditor::CompletionItem &item, QChar typedChar)
1026 Q_UNUSED(typedChar) // Currently always included in the completion item when used
1028 QString toInsert = item.text;
1030 if (QmlJSTextEditorWidget *edit = qobject_cast<QmlJSTextEditorWidget *>(m_editor->widget())) {
1031 if (item.data.isValid()) {
1032 QTextCursor tc = edit->textCursor();
1033 tc.setPosition(m_startPosition, QTextCursor::KeepAnchor);
1034 toInsert = item.data.toString();
1035 edit->insertCodeSnippet(tc, toInsert);
1040 QString replacableChars;
1041 if (toInsert.endsWith(QLatin1String(": ")))
1042 replacableChars = QLatin1String(": ");
1043 else if (toInsert.endsWith(QLatin1Char('.')))
1044 replacableChars = QLatin1String(".");
1046 int replacedLength = 0;
1048 // Avoid inserting characters that are already there
1049 for (int i = 0; i < replacableChars.length(); ++i) {
1050 const QChar a = replacableChars.at(i);
1051 const QChar b = m_editor->characterAt(m_editor->position() + i);
1058 const int length = m_editor->position() - m_startPosition + replacedLength;
1059 m_editor->setCursorPosition(m_startPosition);
1060 m_editor->replace(length, toInsert);
1062 if (toInsert.endsWith(QLatin1Char('.')))
1063 m_restartCompletion = true;
1066 bool CodeCompletion::partiallyComplete(const QList<TextEditor::CompletionItem> &completionItems)
1068 if (completionItems.count() == 1) {
1069 const TextEditor::CompletionItem item = completionItems.first();
1071 if (!item.data.canConvert<QString>()) {
1072 complete(item, QChar());
1077 return TextEditor::ICompletionCollector::partiallyComplete(completionItems);
1080 void CodeCompletion::cleanup()
1083 m_startPosition = 0;
1084 m_completions.clear();
1087 static bool qmlCompletionItemLessThan(const TextEditor::CompletionItem &l, const TextEditor::CompletionItem &r)
1089 if (l.order != r.order)
1090 return l.order > r.order;
1091 else if (l.text.isEmpty())
1093 else if (r.text.isEmpty())
1095 else if (l.data.isValid() != r.data.isValid())
1096 return l.data.isValid();
1097 else if (l.text.at(0).isUpper() && r.text.at(0).isLower())
1099 else if (l.text.at(0).isLower() && r.text.at(0).isUpper())
1102 return l.text < r.text;
1105 void CodeCompletion::sortCompletion(QList<TextEditor::CompletionItem> &completionItems)
1107 qStableSort(completionItems.begin(), completionItems.end(), qmlCompletionItemLessThan);
1110 QList<TextEditor::CompletionItem> CodeCompletion::getCompletions()
1112 QList<TextEditor::CompletionItem> completionItems;
1114 completions(&completionItems);
1116 sortCompletion(completionItems);
1118 // Remove duplicates
1121 QList<TextEditor::CompletionItem> uniquelist;
1123 foreach (const TextEditor::CompletionItem &item, completionItems) {
1124 if (item.text != lastKey || item.data.type() != lastData.type()) {
1125 uniquelist.append(item);
1126 lastKey = item.text;
1127 lastData = item.data;