qmljscodecompletion.cpp 31.8 KB
Newer Older
1
2
3
4
/**************************************************************************
**
** This file is part of Qt Creator
**
hjk's avatar
hjk committed
5
** Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies).
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
**
** Contact: Nokia Corporation (qt-info@nokia.com)
**
** Commercial Usage
**
** Licensees holding valid Qt Commercial licenses may use this file in
** accordance with the Qt Commercial License Agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Nokia.
**
** GNU Lesser General Public License Usage
**
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** If you are unsure which license is appropriate for your use, please
** contact the sales department at http://qt.nokia.com/contact.
**
**************************************************************************/

Roberto Raggi's avatar
Roberto Raggi committed
30
#include "qmljscodecompletion.h"
31
#include "qmlexpressionundercursor.h"
32
#include "qmljseditor.h"
Roberto Raggi's avatar
Roberto Raggi committed
33
#include "qmljsmodelmanagerinterface.h"
34

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

42
#include <texteditor/basetexteditor.h>
43

44
#include <coreplugin/icore.h>
45
#include <coreplugin/editormanager/editormanager.h>
46

47
48
#include <utils/faketooltip.h>

49
50
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
51
#include <QtCore/QDir>
52
#include <QtCore/QXmlStreamReader>
53
#include <QtCore/QDebug>
54

55
56
57
#include <QtGui/QApplication>
#include <QtGui/QDesktopWidget>
#include <QtGui/QHBoxLayout>
58
#include <QtGui/QLabel>
59
#include <QtGui/QPainter>
60
#include <QtGui/QStyle>
61
#include <QtGui/QTextBlock>
62
#include <QtGui/QToolButton>
63

64
65
using namespace QmlJSEditor;
using namespace QmlJSEditor::Internal;
66
using namespace QmlJS;
67

68
namespace {
69
70

// Temporary workaround until we have proper icons for QML completion items
71
QIcon iconForColor(const QColor &color)
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
{
    QPixmap pix(6, 6);

    int pixSize = 20;
    QBrush br(color);

    QPixmap pm(2 * pixSize, 2 * pixSize);
    QPainter pmp(&pm);
    pmp.fillRect(0, 0, pixSize, pixSize, Qt::lightGray);
    pmp.fillRect(pixSize, pixSize, pixSize, pixSize, Qt::lightGray);
    pmp.fillRect(0, pixSize, pixSize, pixSize, Qt::darkGray);
    pmp.fillRect(pixSize, 0, pixSize, pixSize, Qt::darkGray);
    pmp.fillRect(0, 0, 2 * pixSize, 2 * pixSize, color);
    br = QBrush(pm);

    QPainter p(&pix);
    int corr = 1;
    QRect r = pix.rect().adjusted(corr, corr, -corr, -corr);
    p.setBrushOrigin((r.width() % pixSize + pixSize) / 2 + corr, (r.height() % pixSize + pixSize) / 2 + corr);
    p.fillRect(r, br);

    p.fillRect(r.width() / 4 + corr, r.height() / 4 + corr,
               r.width() / 2, r.height() / 2,
               QColor(color.rgb()));
    p.drawRect(pix.rect().adjusted(0, 0, -1, -1));

    return pix;
}

101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
bool checkStartOfIdentifier(const QString &word)
{
    if (word.isEmpty())
        return false;

    const QChar ch = word.at(0);

    switch (ch.unicode()) {
    case '_': case '$':
        return true;

    default:
        return ch.isLetter();
    }
}

bool isIdentifierChar(QChar ch)
{
    switch (ch.unicode()) {
    case '_': case '$':
        return true;

    default:
        return ch.isLetterOrNumber();
    }
}
127

128
129
130
131
132
class SearchPropertyDefinitions: protected AST::Visitor
{
    QList<AST::UiPublicMember *> _properties;

public:
133
    QList<AST::UiPublicMember *> operator()(Document::Ptr doc)
134
135
    {
        _properties.clear();
136
137
        if (doc && doc->qmlProgram())
            doc->qmlProgram()->accept(this);
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
        return _properties;
    }


protected:
    using AST::Visitor::visit;

    virtual bool visit(AST::UiPublicMember *member)
    {
        if (member->propertyToken.isValid()) {
            _properties.append(member);
        }

        return true;
    }
};

Roberto Raggi's avatar
Cleanup  
Roberto Raggi committed
155
class EnumerateProperties: private Interpreter::MemberProcessor
156
157
158
{
    QSet<const Interpreter::ObjectValue *> _processed;
    QHash<QString, const Interpreter::Value *> _properties;
159
    bool _globalCompletion;
Christian Kamm's avatar
Christian Kamm committed
160
    bool _enumerateGeneratedSlots;
161
    Interpreter::Context *_context;
162
    const Interpreter::ObjectValue *_currentObject;
163
164

public:
165
    EnumerateProperties(Interpreter::Context *context)
166
        : _globalCompletion(false),
Christian Kamm's avatar
Christian Kamm committed
167
          _enumerateGeneratedSlots(false),
168
169
          _context(context),
          _currentObject(0)
170
171
172
173
174
175
176
177
    {
    }

    void setGlobalCompletion(bool globalCompletion)
    {
        _globalCompletion = globalCompletion;
    }

Christian Kamm's avatar
Christian Kamm committed
178
179
180
181
182
    void setEnumerateGeneratedSlots(bool enumerate)
    {
        _enumerateGeneratedSlots = enumerate;
    }

183
    QHash<QString, const Interpreter::Value *> operator ()(const Interpreter::Value *value)
184
185
186
    {
        _processed.clear();
        _properties.clear();
187
        _currentObject = Interpreter::value_cast<const Interpreter::ObjectValue *>(value);
188
189
190
191
192
193
194
195
196
197

        enumerateProperties(value);

        return _properties;
    }

    QHash<QString, const Interpreter::Value *> operator ()()
    {
        _processed.clear();
        _properties.clear();
198
        _currentObject = 0;
199

200
        foreach (const Interpreter::ObjectValue *scope, _context->scopeChain().all())
201
202
            enumerateProperties(scope);

203
204
205
206
        return _properties;
    }

private:
207
208
209
210
211
212
213
    void insertProperty(const QString &name, const Interpreter::Value *value)
    {
        if (_context->lookupMode() == Interpreter::Context::JSLookup ||
                ! dynamic_cast<const Interpreter::ASTVariableReference *>(value))
            _properties.insert(name, value);
    }

214
    virtual bool processProperty(const QString &name, const Interpreter::Value *value)
Roberto Raggi's avatar
Cleanup  
Roberto Raggi committed
215
    {
216
        insertProperty(name, value);
Roberto Raggi's avatar
Cleanup  
Roberto Raggi committed
217
218
219
        return true;
    }

220
221
222
    virtual bool processEnumerator(const QString &name, const Interpreter::Value *value)
    {
        if (! _globalCompletion)
223
            insertProperty(name, value);
224
225
226
        return true;
    }

227
    virtual bool processSignal(const QString &, const Interpreter::Value *)
228
229
230
231
    {
        return true;
    }

232
    virtual bool processSlot(const QString &name, const Interpreter::Value *value)
233
234
    {
        if (! _globalCompletion)
235
            insertProperty(name, value);
236
237
238
239
        return true;
    }

    virtual bool processGeneratedSlot(const QString &name, const Interpreter::Value *value)
240
    {
Christian Kamm's avatar
Christian Kamm committed
241
        if (_enumerateGeneratedSlots || (_currentObject && _currentObject->className().endsWith(QLatin1String("Keys")))) {
242
            // ### FIXME: add support for attached properties.
243
            insertProperty(name, value);
244
        }
245
246
247
        return true;
    }

248
    void enumerateProperties(const Interpreter::Value *value)
249
250
251
252
    {
        if (! value)
            return;
        else if (const Interpreter::ObjectValue *object = value->asObjectValue()) {
253
            enumerateProperties(object);
254
255
256
        }
    }

257
    void enumerateProperties(const Interpreter::ObjectValue *object)
258
259
260
261
262
    {
        if (! object || _processed.contains(object))
            return;

        _processed.insert(object);
263
        enumerateProperties(object->prototype(_context));
264

Roberto Raggi's avatar
Cleanup  
Roberto Raggi committed
265
        object->processMembers(this);
266
267
268
269
270
    }
};

} // end of anonymous namespace

271
272
273
274
275
276
277
namespace QmlJSEditor {
namespace Internal {

class FunctionArgumentWidget : public QLabel
{
public:
    FunctionArgumentWidget();
278
279
280
    void showFunctionHint(const QString &functionName,
                          const QStringList &signature,
                          int startPosition);
281
282
283
284
285
286
287
288
289

protected:
    bool eventFilter(QObject *obj, QEvent *e);

private:
    void updateArgumentHighlight();
    void updateHintText();

    QString m_functionName;
290
    QStringList m_signature;
291
292
293
294
295
296
297
298
299
300
    int m_minimumArgumentCount;
    int m_startpos;
    int m_currentarg;
    int m_current;
    bool m_escapePressed;

    TextEditor::ITextEditor *m_editor;

    QWidget *m_pager;
    QLabel *m_numberLabel;
301
    Utils::FakeToolTip *m_popupFrame;
302
303
304
305
306
307
308
309
310
311
312
313
};


FunctionArgumentWidget::FunctionArgumentWidget():
    m_minimumArgumentCount(0),
    m_startpos(-1),
    m_current(0),
    m_escapePressed(false)
{
    QObject *editorObject = Core::EditorManager::instance()->currentEditor();
    m_editor = qobject_cast<TextEditor::ITextEditor *>(editorObject);

314
    m_popupFrame = new Utils::FakeToolTip(m_editor->widget());
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337

    setParent(m_popupFrame);
    setFocusPolicy(Qt::NoFocus);

    m_pager = new QWidget;
    QHBoxLayout *hbox = new QHBoxLayout(m_pager);
    hbox->setMargin(0);
    hbox->setSpacing(0);
    m_numberLabel = new QLabel;
    hbox->addWidget(m_numberLabel);

    QHBoxLayout *layout = new QHBoxLayout;
    layout->setMargin(0);
    layout->setSpacing(0);
    layout->addWidget(m_pager);
    layout->addWidget(this);
    m_popupFrame->setLayout(layout);

    setTextFormat(Qt::RichText);

    qApp->installEventFilter(this);
}

338
void FunctionArgumentWidget::showFunctionHint(const QString &functionName, const QStringList &signature, int startPosition)
339
340
341
342
343
{
    if (m_startpos == startPosition)
        return;

    m_functionName = functionName;
344
345
    m_signature = signature;
    m_minimumArgumentCount = signature.size();
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
    m_startpos = startPosition;
    m_current = 0;
    m_escapePressed = false;

    // update the text
    m_currentarg = -1;
    updateArgumentHighlight();

    m_popupFrame->show();
}

void FunctionArgumentWidget::updateArgumentHighlight()
{
    int curpos = m_editor->position();
    if (curpos < m_startpos) {
        m_popupFrame->close();
        return;
    }

    updateHintText();

    QString str = m_editor->textAt(m_startpos, curpos - m_startpos);
    int argnr = 0;
    int parcount = 0;
Roberto Raggi's avatar
Roberto Raggi committed
370
    Scanner tokenize;
371
    const QList<Token> tokens = tokenize(str);
372
    for (int i = 0; i < tokens.count(); ++i) {
373
374
        const Token &tk = tokens.at(i);
        if (tk.is(Token::LeftParenthesis))
375
            ++parcount;
376
        else if (tk.is(Token::RightParenthesis))
377
            --parcount;
378
        else if (! parcount && tk.is(Token::Colon))
379
380
381
382
            ++argnr;
    }

    if (m_currentarg != argnr) {
383
        // m_currentarg = argnr;
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
        updateHintText();
    }

    if (parcount < 0)
        m_popupFrame->close();
}

bool FunctionArgumentWidget::eventFilter(QObject *obj, QEvent *e)
{
    switch (e->type()) {
    case QEvent::ShortcutOverride:
        if (static_cast<QKeyEvent*>(e)->key() == Qt::Key_Escape) {
            m_escapePressed = true;
        }
        break;
    case QEvent::KeyPress:
        if (static_cast<QKeyEvent*>(e)->key() == Qt::Key_Escape) {
            m_escapePressed = true;
        }
        break;
    case QEvent::KeyRelease:
        if (static_cast<QKeyEvent*>(e)->key() == Qt::Key_Escape && m_escapePressed) {
            m_popupFrame->close();
            return false;
        }
        updateArgumentHighlight();
        break;
    case QEvent::WindowDeactivate:
    case QEvent::FocusOut:
        if (obj != m_editor->widget())
            break;
        m_popupFrame->close();
        break;
    case QEvent::MouseButtonPress:
    case QEvent::MouseButtonRelease:
    case QEvent::MouseButtonDblClick:
    case QEvent::Wheel: {
            QWidget *widget = qobject_cast<QWidget *>(obj);
            if (! (widget == this || m_popupFrame->isAncestorOf(widget))) {
                m_popupFrame->close();
            }
        }
        break;
    default:
        break;
    }
    return false;
}

void FunctionArgumentWidget::updateHintText()
{
    QString prettyMethod;
    prettyMethod += QString::fromLatin1("function ");
    prettyMethod += m_functionName;
438
439
440
441
442
    prettyMethod += QLatin1Char('(');
    for (int i = 0; i < m_minimumArgumentCount; ++i) {
        if (i != 0)
            prettyMethod += QLatin1String(", ");

Roberto Raggi's avatar
Roberto Raggi committed
443
444
445
446
447
        QString arg = m_signature.at(i);
        if (arg.isEmpty()) {
            arg = QLatin1String("arg");
            arg += QString::number(i + 1);
        }
448

Roberto Raggi's avatar
Roberto Raggi committed
449
        prettyMethod += arg;
450
451
    }
    prettyMethod += QLatin1Char(')');
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475

    m_numberLabel->setText(prettyMethod);

    m_popupFrame->setFixedWidth(m_popupFrame->minimumSizeHint().width());

    const QDesktopWidget *desktop = QApplication::desktop();
#ifdef Q_WS_MAC
    const QRect screen = desktop->availableGeometry(desktop->screenNumber(m_editor->widget()));
#else
    const QRect screen = desktop->screenGeometry(desktop->screenNumber(m_editor->widget()));
#endif

    const QSize sz = m_popupFrame->sizeHint();
    QPoint pos = m_editor->cursorRect(m_startpos).topLeft();
    pos.setY(pos.y() - sz.height() - 1);

    if (pos.x() + sz.width() > screen.right())
        pos.setX(screen.right() - sz.width());

    m_popupFrame->move(pos);
}

} } // end of namespace QmlJSEditor::Internal

Roberto Raggi's avatar
Roberto Raggi committed
476
CodeCompletion::CodeCompletion(ModelManagerInterface *modelManager, QObject *parent)
477
    : TextEditor::ICompletionCollector(parent),
478
      m_modelManager(modelManager),
479
      m_editor(0),
480
      m_startPosition(0)
481
482
483
{
    Q_ASSERT(modelManager);
}
484

Roberto Raggi's avatar
Roberto Raggi committed
485
CodeCompletion::~CodeCompletion()
486
487
{ }

Roberto Raggi's avatar
Roberto Raggi committed
488
TextEditor::ITextEditable *CodeCompletion::editor() const
489
490
{ return m_editor; }

Roberto Raggi's avatar
Roberto Raggi committed
491
int CodeCompletion::startPosition() const
492
493
{ return m_startPosition; }

Roberto Raggi's avatar
Roberto Raggi committed
494
bool CodeCompletion::shouldRestartCompletion()
495
496
{ return false; }

Roberto Raggi's avatar
Roberto Raggi committed
497
bool CodeCompletion::supportsEditor(TextEditor::ITextEditable *editor)
498
{
499
    if (qobject_cast<QmlJSTextEditor *>(editor->widget()))
500
501
502
503
504
        return true;

    return false;
}

Roberto Raggi's avatar
Roberto Raggi committed
505
bool CodeCompletion::triggersCompletion(TextEditor::ITextEditable *editor)
506
507
508
509
510
511
512
513
514
515
516
517
{
    if (maybeTriggersCompletion(editor)) {
        // check the token under cursor

        if (QmlJSTextEditor *ed = qobject_cast<QmlJSTextEditor *>(editor->widget())) {

            QTextCursor tc = ed->textCursor();
            QTextBlock block = tc.block();
            const int column = tc.columnNumber();
            const int blockState = qMax(0, block.previous().userState()) & 0xff;
            const QString blockText = block.text();

Roberto Raggi's avatar
Roberto Raggi committed
518
            Scanner scanner;
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
            const QList<Token> tokens = scanner(blockText, blockState);
            foreach (const Token &tk, tokens) {
                if (column >= tk.begin() && column <= tk.end()) {
                    if (tk.is(Token::Comment) || tk.is(Token::String))
                        return false;
                    else
                        break;
                }
            }
        }
        return true;
    }

    return false;
}

Roberto Raggi's avatar
Roberto Raggi committed
535
bool CodeCompletion::maybeTriggersCompletion(TextEditor::ITextEditable *editor)
536
{
537
538
    const int cursorPosition = editor->position();
    const QChar ch = editor->characterAt(cursorPosition - 1);
539
540
541

    if (ch == QLatin1Char('(') || ch == QLatin1Char('.'))
        return true;
542
543
544
545

    const QChar characterUnderCursor = editor->characterAt(cursorPosition);

    if (isIdentifierChar(ch) && (characterUnderCursor.isSpace() ||
546
547
                                      characterUnderCursor.isNull() ||
                                      isDelimiter(characterUnderCursor))) {
548
549
550
551
552
553
554
555
556
557
558
559
        int pos = editor->position() - 1;
        for (; pos != -1; --pos) {
            if (! isIdentifierChar(editor->characterAt(pos)))
                break;
        }
        ++pos;

        const QString word = editor->textAt(pos, cursorPosition - pos);
        if (word.length() > 2 && checkStartOfIdentifier(word)) {
            for (int i = 0; i < word.length(); ++i) {
                if (! isIdentifierChar(word.at(i)))
                    return false;
560
            }
561
            return true;
562
563
        }
    }
564
565
566

    return false;
}
567

Roberto Raggi's avatar
Roberto Raggi committed
568
bool CodeCompletion::isDelimiter(const QChar &ch) const
569
570
571
572
573
574
{
    switch (ch.unicode()) {
    case '{':
    case '}':
    case '[':
    case ']':
575
    case ')':
576
    case '?':
577
    case '!':
578
579
580
581
582
583
584
585
586
587
    case ':':
    case ';':
    case ',':
        return true;

    default:
        return false;
    }
}

588
589
590
591
592
593
594
595
596
597
static bool isLiteral(AST::Node *ast)
{
    if (AST::cast<AST::StringLiteral *>(ast))
        return true;
    else if (AST::cast<AST::NumericLiteral *>(ast))
        return true;
    else
        return false;
}

Roberto Raggi's avatar
Roberto Raggi committed
598
int CodeCompletion::startCompletion(TextEditor::ITextEditable *editor)
599
600
601
{
    m_editor = editor;

602
    QmlJSTextEditor *edit = qobject_cast<QmlJSTextEditor *>(m_editor->widget());
603
604
605
    if (! edit)
        return -1;

606
    m_startPosition = editor->position();
Roberto Raggi's avatar
Roberto Raggi committed
607
    const QString fileName = editor->file()->fileName();
608

Roberto Raggi's avatar
Roberto Raggi committed
609
610
    while (editor->characterAt(m_startPosition - 1).isLetterOrNumber() ||
           editor->characterAt(m_startPosition - 1) == QLatin1Char('_'))
611
        --m_startPosition;
612
613
614

    m_completions.clear();

Roberto Raggi's avatar
Cleanup  
Roberto Raggi committed
615
    const SemanticInfo semanticInfo = edit->semanticInfo();
616
    const QmlJS::Snapshot snapshot = semanticInfo.snapshot;
617
    const Document::Ptr document = semanticInfo.document;
618
619

    const QFileInfo currentFileInfo(fileName);
620

621
622
623
624
    bool isQmlFile = false;
    if (currentFileInfo.suffix() == QLatin1String("qml"))
        isQmlFile = true;

625
    const QIcon symbolIcon = iconForColor(Qt::darkCyan);
626
    const QIcon keywordIcon = iconForColor(Qt::darkYellow);
627
628

    Interpreter::Engine interp;
629
    Interpreter::Context context(&interp);
630

Roberto Raggi's avatar
Roberto Raggi committed
631
    // Set up the current scope chain.
632
    QList<AST::Node *> astPath = semanticInfo.astPath(editor->position());
633
    context.build(astPath , document, snapshot, m_modelManager->importPaths());
634

Roberto Raggi's avatar
Roberto Raggi committed
635
    // Search for the operator that triggered the completion.
636
    QChar completionOperator;
637
    if (m_startPosition > 0)
638
        completionOperator = editor->characterAt(m_startPosition - 1);
639

640
641
    if (completionOperator.isSpace() || completionOperator.isNull() || isDelimiter(completionOperator) ||
            (completionOperator == QLatin1Char('(') && m_startPosition != editor->position())) {
642

Christian Kamm's avatar
Christian Kamm committed
643
644
645
646
        bool doGlobalCompletion = true;
        bool doQmlKeywordCompletion = true;
        bool doJsKeywordCompletion = true;

647
648
649
        QTextCursor startPositionCursor(edit->document());
        startPositionCursor.setPosition(m_startPosition);
        CompletionContextFinder contextFinder(startPositionCursor);
650
651
652
653
654
655
656

        const Interpreter::ObjectValue *qmlScopeType = 0;
        if (contextFinder.isInQmlContext())
             qmlScopeType = context.lookupType(document.data(), contextFinder.qmlObjectTypeName());

        if (contextFinder.isInLhsOfBinding() && qmlScopeType) {
            doGlobalCompletion = false;
Christian Kamm's avatar
Christian Kamm committed
657
658
            doJsKeywordCompletion = false;

659
660
            EnumerateProperties enumerateProperties(&context);
            enumerateProperties.setGlobalCompletion(true);
Christian Kamm's avatar
Christian Kamm committed
661
            enumerateProperties.setEnumerateGeneratedSlots(true);
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681

            QHashIterator<QString, const Interpreter::Value *> it(enumerateProperties(qmlScopeType));
            while (it.hasNext()) {
                it.next();

                TextEditor::CompletionItem item(this);
                item.text = it.key();
                item.icon = symbolIcon;
                m_completions.append(item);
            }

            it = enumerateProperties(context.scopeChain().qmlTypes);
            while (it.hasNext()) {
                it.next();

                TextEditor::CompletionItem item(this);
                item.text = it.key();
                item.icon = symbolIcon;
                m_completions.append(item);
            }
682
        }
683

Christian Kamm's avatar
Christian Kamm committed
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
        if (contextFinder.isInRhsOfBinding() && qmlScopeType) {
            doQmlKeywordCompletion = false;

            if (!contextFinder.bindingPropertyName().isEmpty()) {
                const Interpreter::Value *value = qmlScopeType;
                foreach (const QString &name, contextFinder.bindingPropertyName()) {
                    if (const Interpreter::ObjectValue *objectValue = value->asObjectValue()) {
                        value = objectValue->property(name, &context);
                        if (!value)
                            break;
                    } else {
                        value = 0;
                        break;
                    }
                }

                if (const Interpreter::QmlEnumValue *enumValue = dynamic_cast<const Interpreter::QmlEnumValue *>(value)) {
                    foreach (const QString &key, enumValue->keys()) {
                        TextEditor::CompletionItem item(this);
                        item.text = key;
                        item.data = QString("\"%1\"").arg(key);
                        item.icon = symbolIcon;
                        m_completions.append(item);
                    }
                }
            }
        }

712
713
714
715
716
717
718
719
720
721
722
723
724
        if (doGlobalCompletion) {
            // It's a global completion.
            EnumerateProperties enumerateProperties(&context);
            enumerateProperties.setGlobalCompletion(true);
            QHashIterator<QString, const Interpreter::Value *> it(enumerateProperties());
            while (it.hasNext()) {
                it.next();

                TextEditor::CompletionItem item(this);
                item.text = it.key();
                item.icon = symbolIcon;
                m_completions.append(item);
            }
Christian Kamm's avatar
Christian Kamm committed
725
        }
726

Christian Kamm's avatar
Christian Kamm committed
727
        if (doJsKeywordCompletion) {
728
729
730
731
732
733
734
            // add js keywords
            foreach (const QString &word, Scanner::keywords()) {
                TextEditor::CompletionItem item(this);
                item.text = word;
                item.icon = keywordIcon;
                m_completions.append(item);
            }
735
736
737
        }

        // add qml extra words
Christian Kamm's avatar
Christian Kamm committed
738
        if (doQmlKeywordCompletion && isQmlFile) {
739
            static QStringList qmlWords;
740
741
742
743
744
745
746
747

            if (qmlWords.isEmpty()) {
                qmlWords << QLatin1String("property")
                        << QLatin1String("readonly")
                        << QLatin1String("signal")
                        << QLatin1String("import");
            }

748
749
750
            foreach (const QString &word, qmlWords) {
                TextEditor::CompletionItem item(this);
                item.text = word;
751
                item.icon = keywordIcon;
752
753
                m_completions.append(item);
            }
Christian Kamm's avatar
Christian Kamm committed
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769

            if (!doJsKeywordCompletion) {
                {
                    TextEditor::CompletionItem item(this);
                    item.text = QLatin1String("default");
                    item.icon = keywordIcon;
                    m_completions.append(item);
                }

                {
                    TextEditor::CompletionItem item(this);
                    item.text = QLatin1String("function");
                    item.icon = keywordIcon;
                    m_completions.append(item);
                }
            }
770
        }
771
772
    }

773
    else if (completionOperator == QLatin1Char('.') || completionOperator == QLatin1Char('(')) {
Roberto Raggi's avatar
Roberto Raggi committed
774
        // Look at the expression under cursor.
775
776
        QTextCursor tc = edit->textCursor();
        tc.setPosition(m_startPosition - 1);
777

778
779
        QmlExpressionUnderCursor expressionUnderCursor;
        QmlJS::AST::ExpressionNode *expression = expressionUnderCursor(tc);
780

781
        if (expression != 0 && ! isLiteral(expression)) {
782
            Evaluate evaluate(&context);
783

Roberto Raggi's avatar
Roberto Raggi committed
784
            // Evaluate the expression under cursor.
785
            const Interpreter::Value *value = interp.convertToObject(evaluate(expression));
786
787
            //qDebug() << "type:" << interp.typeId(value);

788
            if (value && completionOperator == QLatin1Char('.')) { // member completion
789
                EnumerateProperties enumerateProperties(&context);
790
                QHashIterator<QString, const Interpreter::Value *> it(enumerateProperties(value));
791
792
793
794
795
796
797
798
                while (it.hasNext()) {
                    it.next();

                    TextEditor::CompletionItem item(this);
                    item.text = it.key();
                    item.icon = symbolIcon;
                    m_completions.append(item);
                }
799
800
            } else if (value && completionOperator == QLatin1Char('(') && m_startPosition == editor->position()) {
                // function completion
801
                if (const Interpreter::FunctionValue *f = value->asFunctionValue()) {
802
803
                    QString functionName = expressionUnderCursor.text();
                    int indexOfDot = functionName.lastIndexOf(QLatin1Char('.'));
804
                    if (indexOfDot != -1)
805
                        functionName = functionName.mid(indexOfDot + 1);
806

807
808
809
810
                    // Recreate if necessary
                    if (!m_functionArgumentWidget)
                        m_functionArgumentWidget = new QmlJSEditor::Internal::FunctionArgumentWidget;

811
812
813
814
815
816
                    QStringList signature;
                    for (int i = 0; i < f->argumentCount(); ++i)
                        signature.append(f->argumentName(i));

                    m_functionArgumentWidget->showFunctionHint(functionName.trimmed(),
                                                               signature,
817
                                                               m_startPosition);
818
819
                }

820
                return -1; // We always return -1 when completing function prototypes.
821
822
            }
        }
823
824
825
826
827

        if (! m_completions.isEmpty())
            return m_startPosition;

        return -1;
828
829
    }

830
831
832
    if (isQmlFile && (completionOperator.isNull() || completionOperator.isSpace() || isDelimiter(completionOperator))) {
        updateSnippets();
        m_completions.append(m_snippets);
833
    }
834

835
836
837
838
    if (! m_completions.isEmpty())
        return m_startPosition;

    return -1;
839
840
}

Roberto Raggi's avatar
Roberto Raggi committed
841
void CodeCompletion::completions(QList<TextEditor::CompletionItem> *completions)
842
843
844
845
846
847
848
849
{
    const int length = m_editor->position() - m_startPosition;

    if (length == 0)
        *completions = m_completions;
    else if (length > 0) {
        const QString key = m_editor->textAt(m_startPosition, length);

850
        filter(m_completions, completions, key);
851
852
853
854
855

        if (completions->size() == 1) {
            if (key == completions->first().text)
                completions->clear();
        }
856
857
858
    }
}

Roberto Raggi's avatar
Roberto Raggi committed
859
void CodeCompletion::complete(const TextEditor::CompletionItem &item)
860
{
861
862
863
864
865
    QString toInsert = item.text;

    if (QmlJSTextEditor *edit = qobject_cast<QmlJSTextEditor *>(m_editor->widget())) {
        if (item.data.isValid()) {
            QTextCursor tc = edit->textCursor();
mae's avatar
mae committed
866
            tc.setPosition(m_startPosition, QTextCursor::KeepAnchor);
867
            toInsert = item.data.toString();
mae's avatar
mae committed
868
            edit->insertCodeSnippet(tc, toInsert);
869
870
871
872
            return;
        }
    }

873
874
875
876
877
    const int length = m_editor->position() - m_startPosition;
    m_editor->setCurPos(m_startPosition);
    m_editor->replace(length, toInsert);
}

Roberto Raggi's avatar
Roberto Raggi committed
878
bool CodeCompletion::partiallyComplete(const QList<TextEditor::CompletionItem> &completionItems)
879
880
{
    if (completionItems.count() == 1) {
881
        const TextEditor::CompletionItem item = completionItems.first();
882

883
884
885
        if (!item.data.canConvert<QString>()) {
            complete(item);
            return true;
886
887
        }
    }
888

889
    return TextEditor::ICompletionCollector::partiallyComplete(completionItems);
890
891
}

Roberto Raggi's avatar
Roberto Raggi committed
892
void CodeCompletion::cleanup()
893
894
895
896
897
898
{
    m_editor = 0;
    m_startPosition = 0;
    m_completions.clear();
}

899

Roberto Raggi's avatar
Roberto Raggi committed
900
void CodeCompletion::updateSnippets()
901
902
903
904
905
906
907
908
909
{
    QString qmlsnippets = Core::ICore::instance()->resourcePath() + QLatin1String("/snippets/qml.xml");
    if (!QFile::exists(qmlsnippets))
        return;

    QDateTime lastModified = QFileInfo(qmlsnippets).lastModified();
    if (!m_snippetFileLastModified.isNull() &&  lastModified == m_snippetFileLastModified)
        return;

910
911
    const QIcon icon = iconForColor(Qt::red);

912
913
914
915
    m_snippetFileLastModified = lastModified;
    QFile file(qmlsnippets);
    file.open(QIODevice::ReadOnly);
    QXmlStreamReader xml(&file);
916
    if (xml.readNextStartElement()) {
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
        if (xml.name() == QLatin1String("snippets")) {
            while (xml.readNextStartElement()) {
                if (xml.name() == QLatin1String("snippet")) {
                    TextEditor::CompletionItem item(this);
                    QString title, data;
                    QString description = xml.attributes().value("description").toString();

                    while (!xml.atEnd()) {
                        xml.readNext();
                        if (xml.isEndElement()) {
                            int i = 0;
                            while (i < data.size() && data.at(i).isLetterOrNumber())
                                ++i;
                            title = data.left(i);
                            item.text = title;
                            if (!description.isEmpty()) {
                                item.text +=  QLatin1Char(' ');
                                item.text += description;
                            }
                            item.data = QVariant::fromValue(data);
mae's avatar
mae committed
937
938


mae's avatar
mae committed
939
940
941
942
943
                            QString infotip = data;
                            while (infotip.size() && infotip.at(infotip.size()-1).isSpace())
                                infotip.chop(1);
                            infotip.replace(QLatin1Char('\n'), QLatin1String("<br>"));
                            infotip.replace(QLatin1Char(' '), QLatin1String("&nbsp;"));
mae's avatar
mae committed
944
945
946
                            {
                                QString s = QLatin1String("<nobr>");
                                int count = 0;
mae's avatar
mae committed
947
948
949
                                for (int i = 0; i < infotip.count(); ++i) {
                                    if (infotip.at(i) != QChar::ObjectReplacementCharacter) {
                                        s += infotip.at(i);
mae's avatar
mae committed
950
951
952
953
954
                                        continue;
                                    }
                                    if (++count % 2) {
                                        s += QLatin1String("<b>");
                                    } else {
mae's avatar
mae committed
955
956
                                        if (infotip.at(i-1) == QChar::ObjectReplacementCharacter)
                                            s += QLatin1String("...");
mae's avatar
mae committed
957
958
959
                                        s += QLatin1String("</b>");
                                    }
                                }
mae's avatar
mae committed
960
                                infotip = s;
mae's avatar
mae committed
961
962
                            }

mae's avatar
mae committed
963
                            item.details = infotip;
mae's avatar
mae committed
964

965
                            item.icon = icon;
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
                            m_snippets.append(item);
                            break;
                        }

                        if (xml.isCharacters())
                            data += xml.text();
                        else if (xml.isStartElement()) {
                            if (xml.name() != QLatin1String("tab"))
                                xml.raiseError(QLatin1String("invalid snippets file"));
                            else {
                                data += QChar::ObjectReplacementCharacter;
                                data += xml.readElementText();
                                data += QChar::ObjectReplacementCharacter;
                            }
                        }
                    }
982
983
                } else {
                    xml.skipCurrentElement();
984
985
                }
            }
986
987
        } else {
            xml.skipCurrentElement();
988
989
990
        }
    }
    if (xml.hasError())
991
        qWarning() << qmlsnippets << xml.errorString() << xml.lineNumber() << xml.columnNumber();
992
993
    file.close();
}
Roberto Raggi's avatar
Roberto Raggi committed
994

995
996
static bool qmlCompletionItemLessThan(const TextEditor::CompletionItem &l, const TextEditor::CompletionItem &r)
{
997
998
999
1000
    if (l.text.isEmpty())
        return true;
    else if (r.text.isEmpty())
        return false;
1001
1002
    else if (l.data.isValid() != r.data.isValid())
        return l.data.isValid();
1003
    else if (l.text.at(0).isUpper() && r.text.at(0).isLower())
1004
1005
1006
1007
1008
1009
1010
        return false;
    else if (l.text.at(0).isLower() && r.text.at(0).isUpper())
        return true;

    return l.text < r.text;
}

Roberto Raggi's avatar
Roberto Raggi committed
1011
QList<TextEditor::CompletionItem> CodeCompletion::getCompletions()
Roberto Raggi's avatar
Roberto Raggi committed
1012
1013
1014
1015
1016
{
    QList<TextEditor::CompletionItem> completionItems;

    completions(&completionItems);

1017
    qStableSort(completionItems.begin(), completionItems.end(), qmlCompletionItemLessThan);
Roberto Raggi's avatar
Roberto Raggi committed
1018
1019
1020

    // Remove duplicates
    QString lastKey;
1021
    QVariant lastData;
Roberto Raggi's avatar
Roberto Raggi committed
1022
1023
1024
    QList<TextEditor::CompletionItem> uniquelist;

    foreach (const TextEditor::CompletionItem &item, completionItems) {
1025
        if (item.text != lastKey || item.data.type() != lastData.type()) {
Roberto Raggi's avatar
Roberto Raggi committed
1026
1027
            uniquelist.append(item);
            lastKey = item.text;
1028
            lastData = item.data;
Roberto Raggi's avatar
Roberto Raggi committed
1029
1030
1031
1032
1033
        }
    }

    return uniquelist;
}