1 /**************************************************************************
3 ** This file is part of Qt Creator
5 ** Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies).
7 ** Contact: Nokia Corporation (qt-info@nokia.com)
11 ** Licensees holding valid Qt Commercial licenses may use this file in
12 ** accordance with the Qt Commercial License Agreement provided with the
13 ** Software or, alternatively, in accordance with the terms contained in
14 ** a written agreement between you and Nokia.
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 ** If you are unsure which license is appropriate for your use, please
26 ** contact the sales department at http://qt.nokia.com/contact.
28 **************************************************************************/
30 #include "qmljscodecompletion.h"
31 #include "qmlexpressionundercursor.h"
32 #include "qmljseditor.h"
34 #include <qmljs/qmljsmodelmanagerinterface.h>
35 #include <qmljs/parser/qmljsast_p.h>
36 #include <qmljs/qmljsbind.h>
37 #include <qmljs/qmljsinterpreter.h>
38 #include <qmljs/qmljsscanner.h>
39 #include <qmljs/qmljsevaluate.h>
40 #include <qmljs/qmljscompletioncontextfinder.h>
41 #include <qmljs/qmljslink.h>
42 #include <qmljs/qmljsscopebuilder.h>
44 #include <texteditor/basetexteditor.h>
46 #include <coreplugin/icore.h>
47 #include <coreplugin/editormanager/editormanager.h>
49 #include <utils/faketooltip.h>
51 #include <QtCore/QFile>
52 #include <QtCore/QFileInfo>
53 #include <QtCore/QDir>
54 #include <QtCore/QXmlStreamReader>
55 #include <QtCore/QDebug>
57 #include <QtGui/QApplication>
58 #include <QtGui/QDesktopWidget>
59 #include <QtGui/QHBoxLayout>
60 #include <QtGui/QLabel>
61 #include <QtGui/QPainter>
62 #include <QtGui/QStyle>
63 #include <QtGui/QTextBlock>
64 #include <QtGui/QToolButton>
66 using namespace QmlJSEditor;
67 using namespace QmlJSEditor::Internal;
68 using namespace QmlJS;
72 enum CompletionOrder {
81 // Temporary workaround until we have proper icons for QML completion items
82 static QIcon iconForColor(const QColor &color)
89 QPixmap pm(2 * pixSize, 2 * pixSize);
91 pmp.fillRect(0, 0, pixSize, pixSize, Qt::lightGray);
92 pmp.fillRect(pixSize, pixSize, pixSize, pixSize, Qt::lightGray);
93 pmp.fillRect(0, pixSize, pixSize, pixSize, Qt::darkGray);
94 pmp.fillRect(pixSize, 0, pixSize, pixSize, Qt::darkGray);
95 pmp.fillRect(0, 0, 2 * pixSize, 2 * pixSize, color);
100 QRect r = pix.rect().adjusted(corr, corr, -corr, -corr);
101 p.setBrushOrigin((r.width() % pixSize + pixSize) / 2 + corr, (r.height() % pixSize + pixSize) / 2 + corr);
104 p.fillRect(r.width() / 4 + corr, r.height() / 4 + corr,
105 r.width() / 2, r.height() / 2,
106 QColor(color.rgb()));
107 p.drawRect(pix.rect().adjusted(0, 0, -1, -1));
112 static bool checkStartOfIdentifier(const QString &word)
117 const QChar ch = word.at(0);
119 switch (ch.unicode()) {
124 return ch.isLetter();
128 static bool isIdentifierChar(QChar ch)
130 switch (ch.unicode()) {
135 return ch.isLetterOrNumber();
139 class SearchPropertyDefinitions: protected AST::Visitor
141 QList<AST::UiPublicMember *> _properties;
144 QList<AST::UiPublicMember *> operator()(Document::Ptr doc)
147 if (doc && doc->qmlProgram())
148 doc->qmlProgram()->accept(this);
154 using AST::Visitor::visit;
156 virtual bool visit(AST::UiPublicMember *member)
158 if (member->propertyToken.isValid()) {
159 _properties.append(member);
166 class EnumerateProperties: private Interpreter::MemberProcessor
168 QSet<const Interpreter::ObjectValue *> _processed;
169 QHash<QString, const Interpreter::Value *> _properties;
170 bool _globalCompletion;
171 bool _enumerateGeneratedSlots;
172 Interpreter::Context *_context;
173 const Interpreter::ObjectValue *_currentObject;
176 EnumerateProperties(Interpreter::Context *context)
177 : _globalCompletion(false),
178 _enumerateGeneratedSlots(false),
184 void setGlobalCompletion(bool globalCompletion)
186 _globalCompletion = globalCompletion;
189 void setEnumerateGeneratedSlots(bool enumerate)
191 _enumerateGeneratedSlots = enumerate;
194 QHash<QString, const Interpreter::Value *> operator ()(const Interpreter::Value *value)
198 _currentObject = Interpreter::value_cast<const Interpreter::ObjectValue *>(value);
200 enumerateProperties(value);
205 QHash<QString, const Interpreter::Value *> operator ()()
211 foreach (const Interpreter::ObjectValue *scope, _context->scopeChain().all())
212 enumerateProperties(scope);
218 void insertProperty(const QString &name, const Interpreter::Value *value)
220 _properties.insert(name, value);
223 virtual bool processProperty(const QString &name, const Interpreter::Value *value)
225 insertProperty(name, value);
229 virtual bool processEnumerator(const QString &name, const Interpreter::Value *value)
231 if (! _globalCompletion)
232 insertProperty(name, value);
236 virtual bool processSignal(const QString &, const Interpreter::Value *)
241 virtual bool processSlot(const QString &name, const Interpreter::Value *value)
243 insertProperty(name, value);
247 virtual bool processGeneratedSlot(const QString &name, const Interpreter::Value *value)
249 if (_enumerateGeneratedSlots || (_currentObject && _currentObject->className().endsWith(QLatin1String("Keys")))) {
250 // ### FIXME: add support for attached properties.
251 insertProperty(name, value);
256 void enumerateProperties(const Interpreter::Value *value)
260 else if (const Interpreter::ObjectValue *object = value->asObjectValue()) {
261 enumerateProperties(object);
265 void enumerateProperties(const Interpreter::ObjectValue *object)
267 if (! object || _processed.contains(object))
270 _processed.insert(object);
271 enumerateProperties(object->prototype(_context));
273 object->processMembers(this);
277 } // end of anonymous namespace
279 namespace QmlJSEditor {
282 class FunctionArgumentWidget : public QLabel
285 FunctionArgumentWidget();
286 void showFunctionHint(const QString &functionName,
287 const QStringList &signature,
291 bool eventFilter(QObject *obj, QEvent *e);
294 void updateArgumentHighlight();
295 void updateHintText();
297 QString m_functionName;
298 QStringList m_signature;
299 int m_minimumArgumentCount;
303 bool m_escapePressed;
305 TextEditor::ITextEditor *m_editor;
308 QLabel *m_numberLabel;
309 Utils::FakeToolTip *m_popupFrame;
313 FunctionArgumentWidget::FunctionArgumentWidget():
314 m_minimumArgumentCount(0),
317 m_escapePressed(false)
319 QObject *editorObject = Core::EditorManager::instance()->currentEditor();
320 m_editor = qobject_cast<TextEditor::ITextEditor *>(editorObject);
322 m_popupFrame = new Utils::FakeToolTip(m_editor->widget());
324 setParent(m_popupFrame);
325 setFocusPolicy(Qt::NoFocus);
327 m_pager = new QWidget;
328 QHBoxLayout *hbox = new QHBoxLayout(m_pager);
331 m_numberLabel = new QLabel;
332 hbox->addWidget(m_numberLabel);
334 QHBoxLayout *layout = new QHBoxLayout;
335 layout->setMargin(0);
336 layout->setSpacing(0);
337 layout->addWidget(m_pager);
338 layout->addWidget(this);
339 m_popupFrame->setLayout(layout);
341 setTextFormat(Qt::RichText);
343 qApp->installEventFilter(this);
346 void FunctionArgumentWidget::showFunctionHint(const QString &functionName, const QStringList &signature, int startPosition)
348 if (m_startpos == startPosition)
351 m_functionName = functionName;
352 m_signature = signature;
353 m_minimumArgumentCount = signature.size();
354 m_startpos = startPosition;
356 m_escapePressed = false;
360 updateArgumentHighlight();
362 m_popupFrame->show();
365 void FunctionArgumentWidget::updateArgumentHighlight()
367 int curpos = m_editor->position();
368 if (curpos < m_startpos) {
369 m_popupFrame->close();
375 QString str = m_editor->textAt(m_startpos, curpos - m_startpos);
379 const QList<Token> tokens = tokenize(str);
380 for (int i = 0; i < tokens.count(); ++i) {
381 const Token &tk = tokens.at(i);
382 if (tk.is(Token::LeftParenthesis))
384 else if (tk.is(Token::RightParenthesis))
386 else if (! parcount && tk.is(Token::Colon))
390 if (m_currentarg != argnr) {
391 // m_currentarg = argnr;
396 m_popupFrame->close();
399 bool FunctionArgumentWidget::eventFilter(QObject *obj, QEvent *e)
402 case QEvent::ShortcutOverride:
403 if (static_cast<QKeyEvent*>(e)->key() == Qt::Key_Escape) {
404 m_escapePressed = true;
407 case QEvent::KeyPress:
408 if (static_cast<QKeyEvent*>(e)->key() == Qt::Key_Escape) {
409 m_escapePressed = true;
412 case QEvent::KeyRelease:
413 if (static_cast<QKeyEvent*>(e)->key() == Qt::Key_Escape && m_escapePressed) {
414 m_popupFrame->close();
417 updateArgumentHighlight();
419 case QEvent::WindowDeactivate:
420 case QEvent::FocusOut:
421 if (obj != m_editor->widget())
423 m_popupFrame->close();
425 case QEvent::MouseButtonPress:
426 case QEvent::MouseButtonRelease:
427 case QEvent::MouseButtonDblClick:
428 case QEvent::Wheel: {
429 QWidget *widget = qobject_cast<QWidget *>(obj);
430 if (! (widget == this || m_popupFrame->isAncestorOf(widget))) {
431 m_popupFrame->close();
441 void FunctionArgumentWidget::updateHintText()
443 QString prettyMethod;
444 prettyMethod += QString::fromLatin1("function ");
445 prettyMethod += m_functionName;
446 prettyMethod += QLatin1Char('(');
447 for (int i = 0; i < m_minimumArgumentCount; ++i) {
449 prettyMethod += QLatin1String(", ");
451 QString arg = m_signature.at(i);
453 arg = QLatin1String("arg");
454 arg += QString::number(i + 1);
459 prettyMethod += QLatin1Char(')');
461 m_numberLabel->setText(prettyMethod);
463 m_popupFrame->setFixedWidth(m_popupFrame->minimumSizeHint().width());
465 const QDesktopWidget *desktop = QApplication::desktop();
467 const QRect screen = desktop->availableGeometry(desktop->screenNumber(m_editor->widget()));
469 const QRect screen = desktop->screenGeometry(desktop->screenNumber(m_editor->widget()));
472 const QSize sz = m_popupFrame->sizeHint();
473 QPoint pos = m_editor->cursorRect(m_startpos).topLeft();
474 pos.setY(pos.y() - sz.height() - 1);
476 if (pos.x() + sz.width() > screen.right())
477 pos.setX(screen.right() - sz.width());
479 m_popupFrame->move(pos);
482 } } // end of namespace QmlJSEditor::Internal
484 CodeCompletion::CodeCompletion(ModelManagerInterface *modelManager, QObject *parent)
485 : TextEditor::ICompletionCollector(parent),
486 m_modelManager(modelManager),
489 m_restartCompletion(false)
491 Q_ASSERT(modelManager);
494 CodeCompletion::~CodeCompletion()
497 TextEditor::ITextEditable *CodeCompletion::editor() const
500 int CodeCompletion::startPosition() const
501 { return m_startPosition; }
503 bool CodeCompletion::shouldRestartCompletion()
504 { return m_restartCompletion; }
506 bool CodeCompletion::supportsEditor(TextEditor::ITextEditable *editor)
508 if (qobject_cast<QmlJSTextEditor *>(editor->widget()))
514 bool CodeCompletion::triggersCompletion(TextEditor::ITextEditable *editor)
516 if (maybeTriggersCompletion(editor)) {
517 // check the token under cursor
519 if (QmlJSTextEditor *ed = qobject_cast<QmlJSTextEditor *>(editor->widget())) {
521 QTextCursor tc = ed->textCursor();
522 QTextBlock block = tc.block();
523 const int column = tc.positionInBlock();
524 const int blockState = qMax(0, block.previous().userState()) & 0xff;
525 const QString blockText = block.text();
528 const QList<Token> tokens = scanner(blockText, blockState);
529 foreach (const Token &tk, tokens) {
530 if (column >= tk.begin() && column <= tk.end()) {
531 if (tk.is(Token::Comment) || tk.is(Token::String))
544 bool CodeCompletion::maybeTriggersCompletion(TextEditor::ITextEditable *editor)
546 const int cursorPosition = editor->position();
547 const QChar ch = editor->characterAt(cursorPosition - 1);
549 if (ch == QLatin1Char('(') || ch == QLatin1Char('.'))
552 const QChar characterUnderCursor = editor->characterAt(cursorPosition);
554 if (isIdentifierChar(ch) && (characterUnderCursor.isSpace() ||
555 characterUnderCursor.isNull() ||
556 isDelimiter(characterUnderCursor))) {
557 int pos = editor->position() - 1;
558 for (; pos != -1; --pos) {
559 if (! isIdentifierChar(editor->characterAt(pos)))
564 const QString word = editor->textAt(pos, cursorPosition - pos);
565 if (word.length() > 2 && checkStartOfIdentifier(word)) {
566 for (int i = 0; i < word.length(); ++i) {
567 if (! isIdentifierChar(word.at(i)))
577 bool CodeCompletion::isDelimiter(QChar ch) const
579 switch (ch.unicode()) {
597 static bool isLiteral(AST::Node *ast)
599 if (AST::cast<AST::StringLiteral *>(ast))
601 else if (AST::cast<AST::NumericLiteral *>(ast))
607 void CodeCompletion::addCompletions(const QHash<QString, const Interpreter::Value *> &newCompletions,
608 const QIcon &icon, int order)
610 QHashIterator<QString, const Interpreter::Value *> it(newCompletions);
611 while (it.hasNext()) {
614 TextEditor::CompletionItem item(this);
615 item.text = it.key();
618 m_completions.append(item);
622 void CodeCompletion::addCompletions(const QStringList &newCompletions,
623 const QIcon &icon, int order)
625 foreach (const QString &text, newCompletions) {
626 TextEditor::CompletionItem item(this);
630 m_completions.append(item);
634 void CodeCompletion::addCompletionsPropertyLhs(
635 const QHash<QString, const Interpreter::Value *> &newCompletions,
636 const QIcon &icon, int order)
638 QHashIterator<QString, const Interpreter::Value *> it(newCompletions);
639 while (it.hasNext()) {
642 TextEditor::CompletionItem item(this);
643 item.text = it.key();
644 if (const Interpreter::QmlObjectValue *qmlValue = dynamic_cast<const Interpreter::QmlObjectValue *>(it.value())) {
645 // to distinguish "anchors." from "gradient:" we check if the right hand side
646 // type is instantiatable or is the prototype of an instantiatable object
647 if (qmlValue->hasChildInPackage())
648 item.text.append(QLatin1String(": "));
650 item.text.append(QLatin1Char('.'));
652 item.text.append(QLatin1String(": "));
656 m_completions.append(item);
660 int CodeCompletion::startCompletion(TextEditor::ITextEditable *editor)
662 m_restartCompletion = false;
666 QmlJSTextEditor *edit = qobject_cast<QmlJSTextEditor *>(m_editor->widget());
670 m_startPosition = editor->position();
671 const QString fileName = editor->file()->fileName();
673 while (editor->characterAt(m_startPosition - 1).isLetterOrNumber() ||
674 editor->characterAt(m_startPosition - 1) == QLatin1Char('_'))
677 m_completions.clear();
679 const SemanticInfo semanticInfo = edit->semanticInfo();
680 const QmlJS::Snapshot snapshot = semanticInfo.snapshot;
681 const Document::Ptr document = semanticInfo.document;
683 const QFileInfo currentFileInfo(fileName);
685 bool isQmlFile = false;
686 if (currentFileInfo.suffix() == QLatin1String("qml"))
689 const QIcon symbolIcon = iconForColor(Qt::darkCyan);
690 const QIcon keywordIcon = iconForColor(Qt::darkYellow);
692 Interpreter::Engine interp;
693 Interpreter::Context context(&interp);
694 Link link(&context, document, snapshot, m_modelManager->importPaths());
696 // Set up the current scope chain.
697 ScopeBuilder scopeBuilder(document, &context);
698 scopeBuilder.push(semanticInfo.astPath(editor->position()));
700 // Search for the operator that triggered the completion.
701 QChar completionOperator;
702 if (m_startPosition > 0)
703 completionOperator = editor->characterAt(m_startPosition - 1);
705 QTextCursor startPositionCursor(edit->document());
706 startPositionCursor.setPosition(m_startPosition);
707 CompletionContextFinder contextFinder(startPositionCursor);
709 const Interpreter::ObjectValue *qmlScopeType = 0;
710 if (contextFinder.isInQmlContext())
711 qmlScopeType = context.lookupType(document.data(), contextFinder.qmlObjectTypeName());
713 if (completionOperator.isSpace() || completionOperator.isNull() || isDelimiter(completionOperator) ||
714 (completionOperator == QLatin1Char('(') && m_startPosition != editor->position())) {
716 bool doGlobalCompletion = true;
717 bool doQmlKeywordCompletion = true;
718 bool doJsKeywordCompletion = true;
720 if (contextFinder.isInLhsOfBinding() && qmlScopeType) {
721 doGlobalCompletion = false;
722 doJsKeywordCompletion = false;
724 EnumerateProperties enumerateProperties(&context);
725 enumerateProperties.setGlobalCompletion(true);
726 enumerateProperties.setEnumerateGeneratedSlots(true);
729 TextEditor::CompletionItem idPropertyCompletion(this);
730 idPropertyCompletion.text = QLatin1String("id: ");
731 idPropertyCompletion.icon = symbolIcon;
732 idPropertyCompletion.order = PropertyOrder;
733 m_completions.append(idPropertyCompletion);
735 addCompletionsPropertyLhs(enumerateProperties(qmlScopeType), symbolIcon, PropertyOrder);
736 addCompletions(enumerateProperties(context.scopeChain().qmlTypes), symbolIcon, TypeOrder);
738 if (ScopeBuilder::isPropertyChangesObject(&context, qmlScopeType)
739 && context.scopeChain().qmlScopeObjects.size() == 2) {
740 addCompletions(enumerateProperties(context.scopeChain().qmlScopeObjects.first()), symbolIcon, SymbolOrder);
744 if (contextFinder.isInRhsOfBinding() && qmlScopeType) {
745 doQmlKeywordCompletion = false;
747 if (!contextFinder.bindingPropertyName().isEmpty()) {
748 const Interpreter::Value *value = qmlScopeType;
749 foreach (const QString &name, contextFinder.bindingPropertyName()) {
750 if (const Interpreter::ObjectValue *objectValue = value->asObjectValue()) {
751 value = objectValue->property(name, &context);
760 if (const Interpreter::QmlEnumValue *enumValue = dynamic_cast<const Interpreter::QmlEnumValue *>(value)) {
761 foreach (const QString &key, enumValue->keys()) {
762 TextEditor::CompletionItem item(this);
764 item.data = QString("\"%1\"").arg(key);
765 item.icon = symbolIcon;
766 item.order = EnumValueOrder;
767 m_completions.append(item);
773 if (doGlobalCompletion) {
774 // It's a global completion.
775 EnumerateProperties enumerateProperties(&context);
776 enumerateProperties.setGlobalCompletion(true);
777 addCompletions(enumerateProperties(), symbolIcon, SymbolOrder);
780 if (doJsKeywordCompletion) {
782 addCompletions(Scanner::keywords(), keywordIcon, KeywordOrder);
785 // add qml extra words
786 if (doQmlKeywordCompletion && isQmlFile) {
787 static QStringList qmlWords;
788 static QStringList qmlWordsAlsoInJs;
790 if (qmlWords.isEmpty()) {
791 qmlWords << QLatin1String("property")
792 //<< QLatin1String("readonly")
793 << QLatin1String("signal")
794 << QLatin1String("import");
796 if (qmlWordsAlsoInJs.isEmpty()) {
797 qmlWordsAlsoInJs << QLatin1String("default")
798 << QLatin1String("function");
801 addCompletions(qmlWords, keywordIcon, KeywordOrder);
802 if (!doJsKeywordCompletion)
803 addCompletions(qmlWordsAlsoInJs, keywordIcon, KeywordOrder);
807 else if (completionOperator == QLatin1Char('.') || completionOperator == QLatin1Char('(')) {
808 // Look at the expression under cursor.
809 QTextCursor tc = edit->textCursor();
810 tc.setPosition(m_startPosition - 1);
812 QmlExpressionUnderCursor expressionUnderCursor;
813 QmlJS::AST::ExpressionNode *expression = expressionUnderCursor(tc);
815 if (expression != 0 && ! isLiteral(expression)) {
816 Evaluate evaluate(&context);
818 // Evaluate the expression under cursor.
819 const Interpreter::Value *value = interp.convertToObject(evaluate(expression));
820 //qDebug() << "type:" << interp.typeId(value);
822 if (value && completionOperator == QLatin1Char('.')) { // member completion
823 EnumerateProperties enumerateProperties(&context);
824 if (contextFinder.isInLhsOfBinding() && qmlScopeType && expressionUnderCursor.text().at(0).isLower())
825 addCompletionsPropertyLhs(enumerateProperties(value), symbolIcon, PropertyOrder);
827 addCompletions(enumerateProperties(value), symbolIcon, SymbolOrder);
828 } else if (value && completionOperator == QLatin1Char('(') && m_startPosition == editor->position()) {
829 // function completion
830 if (const Interpreter::FunctionValue *f = value->asFunctionValue()) {
831 QString functionName = expressionUnderCursor.text();
832 int indexOfDot = functionName.lastIndexOf(QLatin1Char('.'));
833 if (indexOfDot != -1)
834 functionName = functionName.mid(indexOfDot + 1);
836 // Recreate if necessary
837 if (!m_functionArgumentWidget)
838 m_functionArgumentWidget = new QmlJSEditor::Internal::FunctionArgumentWidget;
840 QStringList signature;
841 for (int i = 0; i < f->argumentCount(); ++i)
842 signature.append(f->argumentName(i));
844 m_functionArgumentWidget->showFunctionHint(functionName.trimmed(),
849 return -1; // We always return -1 when completing function prototypes.
853 if (! m_completions.isEmpty())
854 return m_startPosition;
859 if (isQmlFile && (completionOperator.isNull() || completionOperator.isSpace() || isDelimiter(completionOperator))) {
861 m_completions.append(m_snippets);
864 if (! m_completions.isEmpty())
865 return m_startPosition;
870 void CodeCompletion::completions(QList<TextEditor::CompletionItem> *completions)
872 const int length = m_editor->position() - m_startPosition;
875 *completions = m_completions;
876 else if (length > 0) {
877 const QString key = m_editor->textAt(m_startPosition, length);
879 filter(m_completions, completions, key);
881 if (completions->size() == 1) {
882 if (key == completions->first().text)
883 completions->clear();
888 void CodeCompletion::complete(const TextEditor::CompletionItem &item)
890 QString toInsert = item.text;
892 if (QmlJSTextEditor *edit = qobject_cast<QmlJSTextEditor *>(m_editor->widget())) {
893 if (item.data.isValid()) {
894 QTextCursor tc = edit->textCursor();
895 tc.setPosition(m_startPosition, QTextCursor::KeepAnchor);
896 toInsert = item.data.toString();
897 edit->insertCodeSnippet(tc, toInsert);
902 QString replacableChars;
903 if (toInsert.endsWith(QLatin1String(": ")))
904 replacableChars = QLatin1String(": ");
905 else if (toInsert.endsWith(QLatin1String(".")))
906 replacableChars = QLatin1String(".");
908 int replacedLength = 0;
910 // Avoid inserting characters that are already there
911 for (int i = 0; i < replacableChars.length(); ++i) {
912 const QChar a = replacableChars.at(i);
913 const QChar b = m_editor->characterAt(m_editor->position() + i);
920 const int length = m_editor->position() - m_startPosition + replacedLength;
921 m_editor->setCurPos(m_startPosition);
922 m_editor->replace(length, toInsert);
924 if (toInsert.endsWith(QLatin1Char('.')))
925 m_restartCompletion = true;
928 bool CodeCompletion::partiallyComplete(const QList<TextEditor::CompletionItem> &completionItems)
930 if (completionItems.count() == 1) {
931 const TextEditor::CompletionItem item = completionItems.first();
933 if (!item.data.canConvert<QString>()) {
939 return TextEditor::ICompletionCollector::partiallyComplete(completionItems);
942 void CodeCompletion::cleanup()
946 m_completions.clear();
950 void CodeCompletion::updateSnippets()
952 QString qmlsnippets = Core::ICore::instance()->resourcePath() + QLatin1String("/snippets/qml.xml");
953 if (!QFile::exists(qmlsnippets))
956 QDateTime lastModified = QFileInfo(qmlsnippets).lastModified();
957 if (!m_snippetFileLastModified.isNull() && lastModified == m_snippetFileLastModified)
960 const QIcon icon = iconForColor(Qt::red);
962 m_snippetFileLastModified = lastModified;
963 QFile file(qmlsnippets);
964 file.open(QIODevice::ReadOnly);
965 QXmlStreamReader xml(&file);
966 if (xml.readNextStartElement()) {
967 if (xml.name() == QLatin1String("snippets")) {
968 while (xml.readNextStartElement()) {
969 if (xml.name() == QLatin1String("snippet")) {
970 TextEditor::CompletionItem item(this);
972 QString description = xml.attributes().value("description").toString();
974 while (!xml.atEnd()) {
976 if (xml.isEndElement()) {
978 while (i < data.size() && data.at(i).isLetterOrNumber())
980 title = data.left(i);
982 if (!description.isEmpty()) {
983 item.text += QLatin1Char(' ');
984 item.text += description;
986 item.data = QVariant::fromValue(data);
989 QString infotip = data;
990 while (infotip.size() && infotip.at(infotip.size()-1).isSpace())
992 infotip.replace(QLatin1Char('\n'), QLatin1String("<br>"));
993 infotip.replace(QLatin1Char(' '), QLatin1String(" "));
995 QString s = QLatin1String("<nobr>");
997 for (int i = 0; i < infotip.count(); ++i) {
998 if (infotip.at(i) != QChar::ObjectReplacementCharacter) {
1003 s += QLatin1String("<b>");
1005 if (infotip.at(i-1) == QChar::ObjectReplacementCharacter)
1006 s += QLatin1String("...");
1007 s += QLatin1String("</b>");
1013 item.details = infotip;
1016 item.order = SnippetOrder;
1017 m_snippets.append(item);
1021 if (xml.isCharacters())
1023 else if (xml.isStartElement()) {
1024 if (xml.name() != QLatin1String("tab"))
1025 xml.raiseError(QLatin1String("invalid snippets file"));
1027 data += QChar::ObjectReplacementCharacter;
1028 data += xml.readElementText();
1029 data += QChar::ObjectReplacementCharacter;
1034 xml.skipCurrentElement();
1038 xml.skipCurrentElement();
1042 qWarning() << qmlsnippets << xml.errorString() << xml.lineNumber() << xml.columnNumber();
1046 static bool qmlCompletionItemLessThan(const TextEditor::CompletionItem &l, const TextEditor::CompletionItem &r)
1048 if (l.order != r.order)
1049 return l.order > r.order;
1050 else if (l.text.isEmpty())
1052 else if (r.text.isEmpty())
1054 else if (l.data.isValid() != r.data.isValid())
1055 return l.data.isValid();
1056 else if (l.text.at(0).isUpper() && r.text.at(0).isLower())
1058 else if (l.text.at(0).isLower() && r.text.at(0).isUpper())
1061 return l.text < r.text;
1064 QList<TextEditor::CompletionItem> CodeCompletion::getCompletions()
1066 QList<TextEditor::CompletionItem> completionItems;
1068 completions(&completionItems);
1070 qStableSort(completionItems.begin(), completionItems.end(), qmlCompletionItemLessThan);
1072 // Remove duplicates
1075 QList<TextEditor::CompletionItem> uniquelist;
1077 foreach (const TextEditor::CompletionItem &item, completionItems) {
1078 if (item.text != lastKey || item.data.type() != lastData.type()) {
1079 uniquelist.append(item);
1080 lastKey = item.text;
1081 lastData = item.data;