OSDN Git Service

Merge remote branch 'origin/2.0'
[qt-creator-jp/qt-creator-jp.git] / src / plugins / qmljseditor / qmljscodecompletion.cpp
1 /**************************************************************************
2 **
3 ** This file is part of Qt Creator
4 **
5 ** Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies).
6 **
7 ** Contact: Nokia Corporation (qt-info@nokia.com)
8 **
9 ** Commercial Usage
10 **
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.
15 **
16 ** GNU Lesser General Public License Usage
17 **
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.
24 **
25 ** If you are unsure which license is appropriate for your use, please
26 ** contact the sales department at http://qt.nokia.com/contact.
27 **
28 **************************************************************************/
29
30 #include "qmljscodecompletion.h"
31 #include "qmlexpressionundercursor.h"
32 #include "qmljseditor.h"
33
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>
43
44 #include <texteditor/basetexteditor.h>
45
46 #include <coreplugin/icore.h>
47 #include <coreplugin/editormanager/editormanager.h>
48
49 #include <utils/faketooltip.h>
50
51 #include <QtCore/QFile>
52 #include <QtCore/QFileInfo>
53 #include <QtCore/QDir>
54 #include <QtCore/QXmlStreamReader>
55 #include <QtCore/QDebug>
56
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>
65
66 using namespace QmlJSEditor;
67 using namespace QmlJSEditor::Internal;
68 using namespace QmlJS;
69
70 namespace {
71
72 enum CompletionOrder {
73     EnumValueOrder = -5,
74     SnippetOrder = -15,
75     PropertyOrder = -10,
76     SymbolOrder = -20,
77     KeywordOrder = -25,
78     TypeOrder = -30
79 };
80
81 // Temporary workaround until we have proper icons for QML completion items
82 static QIcon iconForColor(const QColor &color)
83 {
84     QPixmap pix(6, 6);
85
86     int pixSize = 20;
87     QBrush br(color);
88
89     QPixmap pm(2 * pixSize, 2 * pixSize);
90     QPainter pmp(&pm);
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);
96     br = QBrush(pm);
97
98     QPainter p(&pix);
99     int corr = 1;
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);
102     p.fillRect(r, br);
103
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));
108
109     return pix;
110 }
111
112 static bool checkStartOfIdentifier(const QString &word)
113 {
114     if (word.isEmpty())
115         return false;
116
117     const QChar ch = word.at(0);
118
119     switch (ch.unicode()) {
120     case '_': case '$':
121         return true;
122
123     default:
124         return ch.isLetter();
125     }
126 }
127
128 static bool isIdentifierChar(QChar ch)
129 {
130     switch (ch.unicode()) {
131     case '_': case '$':
132         return true;
133
134     default:
135         return ch.isLetterOrNumber();
136     }
137 }
138
139 class SearchPropertyDefinitions: protected AST::Visitor
140 {
141     QList<AST::UiPublicMember *> _properties;
142
143 public:
144     QList<AST::UiPublicMember *> operator()(Document::Ptr doc)
145     {
146         _properties.clear();
147         if (doc && doc->qmlProgram())
148             doc->qmlProgram()->accept(this);
149         return _properties;
150     }
151
152
153 protected:
154     using AST::Visitor::visit;
155
156     virtual bool visit(AST::UiPublicMember *member)
157     {
158         if (member->propertyToken.isValid()) {
159             _properties.append(member);
160         }
161
162         return true;
163     }
164 };
165
166 class EnumerateProperties: private Interpreter::MemberProcessor
167 {
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;
174
175 public:
176     EnumerateProperties(Interpreter::Context *context)
177         : _globalCompletion(false),
178           _enumerateGeneratedSlots(false),
179           _context(context),
180           _currentObject(0)
181     {
182     }
183
184     void setGlobalCompletion(bool globalCompletion)
185     {
186         _globalCompletion = globalCompletion;
187     }
188
189     void setEnumerateGeneratedSlots(bool enumerate)
190     {
191         _enumerateGeneratedSlots = enumerate;
192     }
193
194     QHash<QString, const Interpreter::Value *> operator ()(const Interpreter::Value *value)
195     {
196         _processed.clear();
197         _properties.clear();
198         _currentObject = Interpreter::value_cast<const Interpreter::ObjectValue *>(value);
199
200         enumerateProperties(value);
201
202         return _properties;
203     }
204
205     QHash<QString, const Interpreter::Value *> operator ()()
206     {
207         _processed.clear();
208         _properties.clear();
209         _currentObject = 0;
210
211         foreach (const Interpreter::ObjectValue *scope, _context->scopeChain().all())
212             enumerateProperties(scope);
213
214         return _properties;
215     }
216
217 private:
218     void insertProperty(const QString &name, const Interpreter::Value *value)
219     {
220         _properties.insert(name, value);
221     }
222
223     virtual bool processProperty(const QString &name, const Interpreter::Value *value)
224     {
225         insertProperty(name, value);
226         return true;
227     }
228
229     virtual bool processEnumerator(const QString &name, const Interpreter::Value *value)
230     {
231         if (! _globalCompletion)
232             insertProperty(name, value);
233         return true;
234     }
235
236     virtual bool processSignal(const QString &, const Interpreter::Value *)
237     {
238         return true;
239     }
240
241     virtual bool processSlot(const QString &name, const Interpreter::Value *value)
242     {
243         insertProperty(name, value);
244         return true;
245     }
246
247     virtual bool processGeneratedSlot(const QString &name, const Interpreter::Value *value)
248     {
249         if (_enumerateGeneratedSlots || (_currentObject && _currentObject->className().endsWith(QLatin1String("Keys")))) {
250             // ### FIXME: add support for attached properties.
251             insertProperty(name, value);
252         }
253         return true;
254     }
255
256     void enumerateProperties(const Interpreter::Value *value)
257     {
258         if (! value)
259             return;
260         else if (const Interpreter::ObjectValue *object = value->asObjectValue()) {
261             enumerateProperties(object);
262         }
263     }
264
265     void enumerateProperties(const Interpreter::ObjectValue *object)
266     {
267         if (! object || _processed.contains(object))
268             return;
269
270         _processed.insert(object);
271         enumerateProperties(object->prototype(_context));
272
273         object->processMembers(this);
274     }
275 };
276
277 } // end of anonymous namespace
278
279 namespace QmlJSEditor {
280 namespace Internal {
281
282 class FunctionArgumentWidget : public QLabel
283 {
284 public:
285     FunctionArgumentWidget();
286     void showFunctionHint(const QString &functionName,
287                           const QStringList &signature,
288                           int startPosition);
289
290 protected:
291     bool eventFilter(QObject *obj, QEvent *e);
292
293 private:
294     void updateArgumentHighlight();
295     void updateHintText();
296
297     QString m_functionName;
298     QStringList m_signature;
299     int m_minimumArgumentCount;
300     int m_startpos;
301     int m_currentarg;
302     int m_current;
303     bool m_escapePressed;
304
305     TextEditor::ITextEditor *m_editor;
306
307     QWidget *m_pager;
308     QLabel *m_numberLabel;
309     Utils::FakeToolTip *m_popupFrame;
310 };
311
312
313 FunctionArgumentWidget::FunctionArgumentWidget():
314     m_minimumArgumentCount(0),
315     m_startpos(-1),
316     m_current(0),
317     m_escapePressed(false)
318 {
319     QObject *editorObject = Core::EditorManager::instance()->currentEditor();
320     m_editor = qobject_cast<TextEditor::ITextEditor *>(editorObject);
321
322     m_popupFrame = new Utils::FakeToolTip(m_editor->widget());
323
324     setParent(m_popupFrame);
325     setFocusPolicy(Qt::NoFocus);
326
327     m_pager = new QWidget;
328     QHBoxLayout *hbox = new QHBoxLayout(m_pager);
329     hbox->setMargin(0);
330     hbox->setSpacing(0);
331     m_numberLabel = new QLabel;
332     hbox->addWidget(m_numberLabel);
333
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);
340
341     setTextFormat(Qt::RichText);
342
343     qApp->installEventFilter(this);
344 }
345
346 void FunctionArgumentWidget::showFunctionHint(const QString &functionName, const QStringList &signature, int startPosition)
347 {
348     if (m_startpos == startPosition)
349         return;
350
351     m_functionName = functionName;
352     m_signature = signature;
353     m_minimumArgumentCount = signature.size();
354     m_startpos = startPosition;
355     m_current = 0;
356     m_escapePressed = false;
357
358     // update the text
359     m_currentarg = -1;
360     updateArgumentHighlight();
361
362     m_popupFrame->show();
363 }
364
365 void FunctionArgumentWidget::updateArgumentHighlight()
366 {
367     int curpos = m_editor->position();
368     if (curpos < m_startpos) {
369         m_popupFrame->close();
370         return;
371     }
372
373     updateHintText();
374
375     QString str = m_editor->textAt(m_startpos, curpos - m_startpos);
376     int argnr = 0;
377     int parcount = 0;
378     Scanner tokenize;
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))
383             ++parcount;
384         else if (tk.is(Token::RightParenthesis))
385             --parcount;
386         else if (! parcount && tk.is(Token::Colon))
387             ++argnr;
388     }
389
390     if (m_currentarg != argnr) {
391         // m_currentarg = argnr;
392         updateHintText();
393     }
394
395     if (parcount < 0)
396         m_popupFrame->close();
397 }
398
399 bool FunctionArgumentWidget::eventFilter(QObject *obj, QEvent *e)
400 {
401     switch (e->type()) {
402     case QEvent::ShortcutOverride:
403         if (static_cast<QKeyEvent*>(e)->key() == Qt::Key_Escape) {
404             m_escapePressed = true;
405         }
406         break;
407     case QEvent::KeyPress:
408         if (static_cast<QKeyEvent*>(e)->key() == Qt::Key_Escape) {
409             m_escapePressed = true;
410         }
411         break;
412     case QEvent::KeyRelease:
413         if (static_cast<QKeyEvent*>(e)->key() == Qt::Key_Escape && m_escapePressed) {
414             m_popupFrame->close();
415             return false;
416         }
417         updateArgumentHighlight();
418         break;
419     case QEvent::WindowDeactivate:
420     case QEvent::FocusOut:
421         if (obj != m_editor->widget())
422             break;
423         m_popupFrame->close();
424         break;
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();
432             }
433         }
434         break;
435     default:
436         break;
437     }
438     return false;
439 }
440
441 void FunctionArgumentWidget::updateHintText()
442 {
443     QString prettyMethod;
444     prettyMethod += QString::fromLatin1("function ");
445     prettyMethod += m_functionName;
446     prettyMethod += QLatin1Char('(');
447     for (int i = 0; i < m_minimumArgumentCount; ++i) {
448         if (i != 0)
449             prettyMethod += QLatin1String(", ");
450
451         QString arg = m_signature.at(i);
452         if (arg.isEmpty()) {
453             arg = QLatin1String("arg");
454             arg += QString::number(i + 1);
455         }
456
457         prettyMethod += arg;
458     }
459     prettyMethod += QLatin1Char(')');
460
461     m_numberLabel->setText(prettyMethod);
462
463     m_popupFrame->setFixedWidth(m_popupFrame->minimumSizeHint().width());
464
465     const QDesktopWidget *desktop = QApplication::desktop();
466 #ifdef Q_WS_MAC
467     const QRect screen = desktop->availableGeometry(desktop->screenNumber(m_editor->widget()));
468 #else
469     const QRect screen = desktop->screenGeometry(desktop->screenNumber(m_editor->widget()));
470 #endif
471
472     const QSize sz = m_popupFrame->sizeHint();
473     QPoint pos = m_editor->cursorRect(m_startpos).topLeft();
474     pos.setY(pos.y() - sz.height() - 1);
475
476     if (pos.x() + sz.width() > screen.right())
477         pos.setX(screen.right() - sz.width());
478
479     m_popupFrame->move(pos);
480 }
481
482 } } // end of namespace QmlJSEditor::Internal
483
484 CodeCompletion::CodeCompletion(ModelManagerInterface *modelManager, QObject *parent)
485     : TextEditor::ICompletionCollector(parent),
486       m_modelManager(modelManager),
487       m_editor(0),
488       m_startPosition(0),
489       m_restartCompletion(false)
490 {
491     Q_ASSERT(modelManager);
492 }
493
494 CodeCompletion::~CodeCompletion()
495 { }
496
497 TextEditor::ITextEditable *CodeCompletion::editor() const
498 { return m_editor; }
499
500 int CodeCompletion::startPosition() const
501 { return m_startPosition; }
502
503 bool CodeCompletion::shouldRestartCompletion()
504 { return m_restartCompletion; }
505
506 bool CodeCompletion::supportsEditor(TextEditor::ITextEditable *editor)
507 {
508     if (qobject_cast<QmlJSTextEditor *>(editor->widget()))
509         return true;
510
511     return false;
512 }
513
514 bool CodeCompletion::triggersCompletion(TextEditor::ITextEditable *editor)
515 {
516     if (maybeTriggersCompletion(editor)) {
517         // check the token under cursor
518
519         if (QmlJSTextEditor *ed = qobject_cast<QmlJSTextEditor *>(editor->widget())) {
520
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();
526
527             Scanner scanner;
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))
532                         return false;
533                     else
534                         break;
535                 }
536             }
537         }
538         return true;
539     }
540
541     return false;
542 }
543
544 bool CodeCompletion::maybeTriggersCompletion(TextEditor::ITextEditable *editor)
545 {
546     const int cursorPosition = editor->position();
547     const QChar ch = editor->characterAt(cursorPosition - 1);
548
549     if (ch == QLatin1Char('(') || ch == QLatin1Char('.'))
550         return true;
551
552     const QChar characterUnderCursor = editor->characterAt(cursorPosition);
553
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)))
560                 break;
561         }
562         ++pos;
563
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)))
568                     return false;
569             }
570             return true;
571         }
572     }
573
574     return false;
575 }
576
577 bool CodeCompletion::isDelimiter(QChar ch) const
578 {
579     switch (ch.unicode()) {
580     case '{':
581     case '}':
582     case '[':
583     case ']':
584     case ')':
585     case '?':
586     case '!':
587     case ':':
588     case ';':
589     case ',':
590         return true;
591
592     default:
593         return false;
594     }
595 }
596
597 static bool isLiteral(AST::Node *ast)
598 {
599     if (AST::cast<AST::StringLiteral *>(ast))
600         return true;
601     else if (AST::cast<AST::NumericLiteral *>(ast))
602         return true;
603     else
604         return false;
605 }
606
607 void CodeCompletion::addCompletions(const QHash<QString, const Interpreter::Value *> &newCompletions,
608                                     const QIcon &icon, int order)
609 {
610     QHashIterator<QString, const Interpreter::Value *> it(newCompletions);
611     while (it.hasNext()) {
612         it.next();
613
614         TextEditor::CompletionItem item(this);
615         item.text = it.key();
616         item.icon = icon;
617         item.order = order;
618         m_completions.append(item);
619     }
620 }
621
622 void CodeCompletion::addCompletions(const QStringList &newCompletions,
623                                     const QIcon &icon, int order)
624 {
625     foreach (const QString &text, newCompletions) {
626         TextEditor::CompletionItem item(this);
627         item.text = text;
628         item.icon = icon;
629         item.order = order;
630         m_completions.append(item);
631     }
632 }
633
634 void CodeCompletion::addCompletionsPropertyLhs(
635         const QHash<QString, const Interpreter::Value *> &newCompletions,
636         const QIcon &icon, int order)
637 {
638     QHashIterator<QString, const Interpreter::Value *> it(newCompletions);
639     while (it.hasNext()) {
640         it.next();
641
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(": "));
649             else
650                 item.text.append(QLatin1Char('.'));
651         } else {
652             item.text.append(QLatin1String(": "));
653         }
654         item.icon = icon;
655         item.order = order;
656         m_completions.append(item);
657     }
658 }
659
660 int CodeCompletion::startCompletion(TextEditor::ITextEditable *editor)
661 {
662     m_restartCompletion = false;
663
664     m_editor = editor;
665
666     QmlJSTextEditor *edit = qobject_cast<QmlJSTextEditor *>(m_editor->widget());
667     if (! edit)
668         return -1;
669
670     m_startPosition = editor->position();
671     const QString fileName = editor->file()->fileName();
672
673     while (editor->characterAt(m_startPosition - 1).isLetterOrNumber() ||
674            editor->characterAt(m_startPosition - 1) == QLatin1Char('_'))
675         --m_startPosition;
676
677     m_completions.clear();
678
679     const SemanticInfo semanticInfo = edit->semanticInfo();
680     const QmlJS::Snapshot snapshot = semanticInfo.snapshot;
681     const Document::Ptr document = semanticInfo.document;
682
683     const QFileInfo currentFileInfo(fileName);
684
685     bool isQmlFile = false;
686     if (currentFileInfo.suffix() == QLatin1String("qml"))
687         isQmlFile = true;
688
689     const QIcon symbolIcon = iconForColor(Qt::darkCyan);
690     const QIcon keywordIcon = iconForColor(Qt::darkYellow);
691
692     Interpreter::Engine interp;
693     Interpreter::Context context(&interp);
694     Link link(&context, document, snapshot, m_modelManager->importPaths());
695
696     // Set up the current scope chain.
697     ScopeBuilder scopeBuilder(document, &context);
698     scopeBuilder.push(semanticInfo.astPath(editor->position()));
699
700     // Search for the operator that triggered the completion.
701     QChar completionOperator;
702     if (m_startPosition > 0)
703         completionOperator = editor->characterAt(m_startPosition - 1);
704
705     QTextCursor startPositionCursor(edit->document());
706     startPositionCursor.setPosition(m_startPosition);
707     CompletionContextFinder contextFinder(startPositionCursor);
708
709     const Interpreter::ObjectValue *qmlScopeType = 0;
710     if (contextFinder.isInQmlContext())
711          qmlScopeType = context.lookupType(document.data(), contextFinder.qmlObjectTypeName());
712
713     if (completionOperator.isSpace() || completionOperator.isNull() || isDelimiter(completionOperator) ||
714             (completionOperator == QLatin1Char('(') && m_startPosition != editor->position())) {
715
716         bool doGlobalCompletion = true;
717         bool doQmlKeywordCompletion = true;
718         bool doJsKeywordCompletion = true;
719
720         if (contextFinder.isInLhsOfBinding() && qmlScopeType) {
721             doGlobalCompletion = false;
722             doJsKeywordCompletion = false;
723
724             EnumerateProperties enumerateProperties(&context);
725             enumerateProperties.setGlobalCompletion(true);
726             enumerateProperties.setEnumerateGeneratedSlots(true);
727
728             // id: is special
729             TextEditor::CompletionItem idPropertyCompletion(this);
730             idPropertyCompletion.text = QLatin1String("id: ");
731             idPropertyCompletion.icon = symbolIcon;
732             idPropertyCompletion.order = PropertyOrder;
733             m_completions.append(idPropertyCompletion);
734
735             addCompletionsPropertyLhs(enumerateProperties(qmlScopeType), symbolIcon, PropertyOrder);
736             addCompletions(enumerateProperties(context.scopeChain().qmlTypes), symbolIcon, TypeOrder);
737
738             if (ScopeBuilder::isPropertyChangesObject(&context, qmlScopeType)
739                     && context.scopeChain().qmlScopeObjects.size() == 2) {
740                 addCompletions(enumerateProperties(context.scopeChain().qmlScopeObjects.first()), symbolIcon, SymbolOrder);
741             }
742         }
743
744         if (contextFinder.isInRhsOfBinding() && qmlScopeType) {
745             doQmlKeywordCompletion = false;
746
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);
752                         if (!value)
753                             break;
754                     } else {
755                         value = 0;
756                         break;
757                     }
758                 }
759
760                 if (const Interpreter::QmlEnumValue *enumValue = dynamic_cast<const Interpreter::QmlEnumValue *>(value)) {
761                     foreach (const QString &key, enumValue->keys()) {
762                         TextEditor::CompletionItem item(this);
763                         item.text = key;
764                         item.data = QString("\"%1\"").arg(key);
765                         item.icon = symbolIcon;
766                         item.order = EnumValueOrder;
767                         m_completions.append(item);
768                     }
769                 }
770             }
771         }
772
773         if (doGlobalCompletion) {
774             // It's a global completion.
775             EnumerateProperties enumerateProperties(&context);
776             enumerateProperties.setGlobalCompletion(true);
777             addCompletions(enumerateProperties(), symbolIcon, SymbolOrder);
778         }
779
780         if (doJsKeywordCompletion) {
781             // add js keywords
782             addCompletions(Scanner::keywords(), keywordIcon, KeywordOrder);
783         }
784
785         // add qml extra words
786         if (doQmlKeywordCompletion && isQmlFile) {
787             static QStringList qmlWords;
788             static QStringList qmlWordsAlsoInJs;
789
790             if (qmlWords.isEmpty()) {
791                 qmlWords << QLatin1String("property")
792                         //<< QLatin1String("readonly")
793                         << QLatin1String("signal")
794                         << QLatin1String("import");
795             }
796             if (qmlWordsAlsoInJs.isEmpty()) {
797                 qmlWordsAlsoInJs << QLatin1String("default")
798                         << QLatin1String("function");
799             }
800
801             addCompletions(qmlWords, keywordIcon, KeywordOrder);
802             if (!doJsKeywordCompletion)
803                 addCompletions(qmlWordsAlsoInJs, keywordIcon, KeywordOrder);
804         }
805     }
806
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);
811
812         QmlExpressionUnderCursor expressionUnderCursor;
813         QmlJS::AST::ExpressionNode *expression = expressionUnderCursor(tc);
814
815         if (expression != 0 && ! isLiteral(expression)) {
816             Evaluate evaluate(&context);
817
818             // Evaluate the expression under cursor.
819             const Interpreter::Value *value = interp.convertToObject(evaluate(expression));
820             //qDebug() << "type:" << interp.typeId(value);
821
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);
826                 else
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);
835
836                     // Recreate if necessary
837                     if (!m_functionArgumentWidget)
838                         m_functionArgumentWidget = new QmlJSEditor::Internal::FunctionArgumentWidget;
839
840                     QStringList signature;
841                     for (int i = 0; i < f->argumentCount(); ++i)
842                         signature.append(f->argumentName(i));
843
844                     m_functionArgumentWidget->showFunctionHint(functionName.trimmed(),
845                                                                signature,
846                                                                m_startPosition);
847                 }
848
849                 return -1; // We always return -1 when completing function prototypes.
850             }
851         }
852
853         if (! m_completions.isEmpty())
854             return m_startPosition;
855
856         return -1;
857     }
858
859     if (isQmlFile && (completionOperator.isNull() || completionOperator.isSpace() || isDelimiter(completionOperator))) {
860         updateSnippets();
861         m_completions.append(m_snippets);
862     }
863
864     if (! m_completions.isEmpty())
865         return m_startPosition;
866
867     return -1;
868 }
869
870 void CodeCompletion::completions(QList<TextEditor::CompletionItem> *completions)
871 {
872     const int length = m_editor->position() - m_startPosition;
873
874     if (length == 0)
875         *completions = m_completions;
876     else if (length > 0) {
877         const QString key = m_editor->textAt(m_startPosition, length);
878
879         filter(m_completions, completions, key);
880
881         if (completions->size() == 1) {
882             if (key == completions->first().text)
883                 completions->clear();
884         }
885     }
886 }
887
888 void CodeCompletion::complete(const TextEditor::CompletionItem &item)
889 {
890     QString toInsert = item.text;
891
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);
898             return;
899         }
900     }
901
902     QString replacableChars;
903     if (toInsert.endsWith(QLatin1String(": ")))
904         replacableChars = QLatin1String(": ");
905     else if (toInsert.endsWith(QLatin1String(".")))
906         replacableChars = QLatin1String(".");
907
908     int replacedLength = 0;
909
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);
914         if (a == b)
915             ++replacedLength;
916         else
917             break;
918     }
919
920     const int length = m_editor->position() - m_startPosition + replacedLength;
921     m_editor->setCurPos(m_startPosition);
922     m_editor->replace(length, toInsert);
923
924     if (toInsert.endsWith(QLatin1Char('.')))
925         m_restartCompletion = true;
926 }
927
928 bool CodeCompletion::partiallyComplete(const QList<TextEditor::CompletionItem> &completionItems)
929 {
930     if (completionItems.count() == 1) {
931         const TextEditor::CompletionItem item = completionItems.first();
932
933         if (!item.data.canConvert<QString>()) {
934             complete(item);
935             return true;
936         }
937     }
938
939     return TextEditor::ICompletionCollector::partiallyComplete(completionItems);
940 }
941
942 void CodeCompletion::cleanup()
943 {
944     m_editor = 0;
945     m_startPosition = 0;
946     m_completions.clear();
947 }
948
949
950 void CodeCompletion::updateSnippets()
951 {
952     QString qmlsnippets = Core::ICore::instance()->resourcePath() + QLatin1String("/snippets/qml.xml");
953     if (!QFile::exists(qmlsnippets))
954         return;
955
956     QDateTime lastModified = QFileInfo(qmlsnippets).lastModified();
957     if (!m_snippetFileLastModified.isNull() &&  lastModified == m_snippetFileLastModified)
958         return;
959
960     const QIcon icon = iconForColor(Qt::red);
961
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);
971                     QString title, data;
972                     QString description = xml.attributes().value("description").toString();
973
974                     while (!xml.atEnd()) {
975                         xml.readNext();
976                         if (xml.isEndElement()) {
977                             int i = 0;
978                             while (i < data.size() && data.at(i).isLetterOrNumber())
979                                 ++i;
980                             title = data.left(i);
981                             item.text = title;
982                             if (!description.isEmpty()) {
983                                 item.text +=  QLatin1Char(' ');
984                                 item.text += description;
985                             }
986                             item.data = QVariant::fromValue(data);
987
988
989                             QString infotip = data;
990                             while (infotip.size() && infotip.at(infotip.size()-1).isSpace())
991                                 infotip.chop(1);
992                             infotip.replace(QLatin1Char('\n'), QLatin1String("<br>"));
993                             infotip.replace(QLatin1Char(' '), QLatin1String("&nbsp;"));
994                             {
995                                 QString s = QLatin1String("<nobr>");
996                                 int count = 0;
997                                 for (int i = 0; i < infotip.count(); ++i) {
998                                     if (infotip.at(i) != QChar::ObjectReplacementCharacter) {
999                                         s += infotip.at(i);
1000                                         continue;
1001                                     }
1002                                     if (++count % 2) {
1003                                         s += QLatin1String("<b>");
1004                                     } else {
1005                                         if (infotip.at(i-1) == QChar::ObjectReplacementCharacter)
1006                                             s += QLatin1String("...");
1007                                         s += QLatin1String("</b>");
1008                                     }
1009                                 }
1010                                 infotip = s;
1011                             }
1012
1013                             item.details = infotip;
1014
1015                             item.icon = icon;
1016                             item.order = SnippetOrder;
1017                             m_snippets.append(item);
1018                             break;
1019                         }
1020
1021                         if (xml.isCharacters())
1022                             data += xml.text();
1023                         else if (xml.isStartElement()) {
1024                             if (xml.name() != QLatin1String("tab"))
1025                                 xml.raiseError(QLatin1String("invalid snippets file"));
1026                             else {
1027                                 data += QChar::ObjectReplacementCharacter;
1028                                 data += xml.readElementText();
1029                                 data += QChar::ObjectReplacementCharacter;
1030                             }
1031                         }
1032                     }
1033                 } else {
1034                     xml.skipCurrentElement();
1035                 }
1036             }
1037         } else {
1038             xml.skipCurrentElement();
1039         }
1040     }
1041     if (xml.hasError())
1042         qWarning() << qmlsnippets << xml.errorString() << xml.lineNumber() << xml.columnNumber();
1043     file.close();
1044 }
1045
1046 static bool qmlCompletionItemLessThan(const TextEditor::CompletionItem &l, const TextEditor::CompletionItem &r)
1047 {
1048     if (l.order != r.order)
1049         return l.order > r.order;
1050     else if (l.text.isEmpty())
1051         return true;
1052     else if (r.text.isEmpty())
1053         return false;
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())
1057         return false;
1058     else if (l.text.at(0).isLower() && r.text.at(0).isUpper())
1059         return true;
1060
1061     return l.text < r.text;
1062 }
1063
1064 QList<TextEditor::CompletionItem> CodeCompletion::getCompletions()
1065 {
1066     QList<TextEditor::CompletionItem> completionItems;
1067
1068     completions(&completionItems);
1069
1070     qStableSort(completionItems.begin(), completionItems.end(), qmlCompletionItemLessThan);
1071
1072     // Remove duplicates
1073     QString lastKey;
1074     QVariant lastData;
1075     QList<TextEditor::CompletionItem> uniquelist;
1076
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;
1082         }
1083     }
1084
1085     return uniquelist;
1086 }