qmljscodecompletion.cpp 33.6 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
#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
50
#include <utils/faketooltip.h>

51
52
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
53
#include <QtCore/QDir>
54
#include <QtCore/QXmlStreamReader>
55
#include <QtCore/QDebug>
56

57
58
59
#include <QtGui/QApplication>
#include <QtGui/QDesktopWidget>
#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
67
using namespace QmlJSEditor;
using namespace QmlJSEditor::Internal;
68
using namespace QmlJS;
69

70
namespace {
71

72
73
74
75
76
77
78
79
80
enum CompletionRelevance {
    EnumValueRelevance = -5,
    SnippetRelevance = -10,
    PropertyRelevance = -15,
    SymbolRelevance = -20,
    KeywordRelevance = -25,
    TypeRelevance = -30
};

81
// Temporary workaround until we have proper icons for QML completion items
82
QIcon iconForColor(const QColor &color)
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
{
    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;
}

112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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();
    }
}
138

139
140
141
142
143
class SearchPropertyDefinitions: protected AST::Visitor
{
    QList<AST::UiPublicMember *> _properties;

public:
144
    QList<AST::UiPublicMember *> operator()(Document::Ptr doc)
145
146
    {
        _properties.clear();
147
148
        if (doc && doc->qmlProgram())
            doc->qmlProgram()->accept(this);
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
        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
166
class EnumerateProperties: private Interpreter::MemberProcessor
167
168
169
{
    QSet<const Interpreter::ObjectValue *> _processed;
    QHash<QString, const Interpreter::Value *> _properties;
170
    bool _globalCompletion;
Christian Kamm's avatar
Christian Kamm committed
171
    bool _enumerateGeneratedSlots;
172
    Interpreter::Context *_context;
173
    const Interpreter::ObjectValue *_currentObject;
174
175

public:
176
    EnumerateProperties(Interpreter::Context *context)
177
        : _globalCompletion(false),
Christian Kamm's avatar
Christian Kamm committed
178
          _enumerateGeneratedSlots(false),
179
180
          _context(context),
          _currentObject(0)
181
182
183
184
185
186
187
188
    {
    }

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

Christian Kamm's avatar
Christian Kamm committed
189
190
191
192
193
    void setEnumerateGeneratedSlots(bool enumerate)
    {
        _enumerateGeneratedSlots = enumerate;
    }

194
    QHash<QString, const Interpreter::Value *> operator ()(const Interpreter::Value *value)
195
196
197
    {
        _processed.clear();
        _properties.clear();
198
        _currentObject = Interpreter::value_cast<const Interpreter::ObjectValue *>(value);
199
200
201
202
203
204
205
206
207
208

        enumerateProperties(value);

        return _properties;
    }

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

211
        foreach (const Interpreter::ObjectValue *scope, _context->scopeChain().all())
212
213
            enumerateProperties(scope);

214
215
216
217
        return _properties;
    }

private:
218
219
220
221
222
223
224
    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);
    }

225
    virtual bool processProperty(const QString &name, const Interpreter::Value *value)
Roberto Raggi's avatar
Cleanup    
Roberto Raggi committed
226
    {
227
        insertProperty(name, value);
Roberto Raggi's avatar
Cleanup    
Roberto Raggi committed
228
229
230
        return true;
    }

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

238
    virtual bool processSignal(const QString &, const Interpreter::Value *)
239
240
241
242
    {
        return true;
    }

243
    virtual bool processSlot(const QString &name, const Interpreter::Value *value)
244
245
    {
        if (! _globalCompletion)
246
            insertProperty(name, value);
247
248
249
250
        return true;
    }

    virtual bool processGeneratedSlot(const QString &name, const Interpreter::Value *value)
251
    {
Christian Kamm's avatar
Christian Kamm committed
252
        if (_enumerateGeneratedSlots || (_currentObject && _currentObject->className().endsWith(QLatin1String("Keys")))) {
253
            // ### FIXME: add support for attached properties.
254
            insertProperty(name, value);
255
        }
256
257
258
        return true;
    }

259
    void enumerateProperties(const Interpreter::Value *value)
260
261
262
263
    {
        if (! value)
            return;
        else if (const Interpreter::ObjectValue *object = value->asObjectValue()) {
264
            enumerateProperties(object);
265
266
267
        }
    }

268
    void enumerateProperties(const Interpreter::ObjectValue *object)
269
270
271
272
273
    {
        if (! object || _processed.contains(object))
            return;

        _processed.insert(object);
274
        enumerateProperties(object->prototype(_context));
275

Roberto Raggi's avatar
Cleanup    
Roberto Raggi committed
276
        object->processMembers(this);
277
278
279
280
281
    }
};

} // end of anonymous namespace

282
283
284
285
286
287
288
namespace QmlJSEditor {
namespace Internal {

class FunctionArgumentWidget : public QLabel
{
public:
    FunctionArgumentWidget();
289
290
291
    void showFunctionHint(const QString &functionName,
                          const QStringList &signature,
                          int startPosition);
292
293
294
295
296
297
298
299
300

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

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

    QString m_functionName;
301
    QStringList m_signature;
302
303
304
305
306
307
308
309
310
311
    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;
312
    Utils::FakeToolTip *m_popupFrame;
313
314
315
316
317
318
319
320
321
322
323
324
};


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);

325
    m_popupFrame = new Utils::FakeToolTip(m_editor->widget());
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348

    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);
}

349
void FunctionArgumentWidget::showFunctionHint(const QString &functionName, const QStringList &signature, int startPosition)
350
351
352
353
354
{
    if (m_startpos == startPosition)
        return;

    m_functionName = functionName;
355
356
    m_signature = signature;
    m_minimumArgumentCount = signature.size();
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
    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
381
    Scanner tokenize;
382
    const QList<Token> tokens = tokenize(str);
383
    for (int i = 0; i < tokens.count(); ++i) {
384
385
        const Token &tk = tokens.at(i);
        if (tk.is(Token::LeftParenthesis))
386
            ++parcount;
387
        else if (tk.is(Token::RightParenthesis))
388
            --parcount;
389
        else if (! parcount && tk.is(Token::Colon))
390
391
392
393
            ++argnr;
    }

    if (m_currentarg != argnr) {
394
        // m_currentarg = argnr;
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
438
439
440
441
442
443
444
445
446
447
448
        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;
449
450
451
452
453
    prettyMethod += QLatin1Char('(');
    for (int i = 0; i < m_minimumArgumentCount; ++i) {
        if (i != 0)
            prettyMethod += QLatin1String(", ");

Roberto Raggi's avatar
Roberto Raggi committed
454
455
456
457
458
        QString arg = m_signature.at(i);
        if (arg.isEmpty()) {
            arg = QLatin1String("arg");
            arg += QString::number(i + 1);
        }
459

Roberto Raggi's avatar
Roberto Raggi committed
460
        prettyMethod += arg;
461
462
    }
    prettyMethod += QLatin1Char(')');
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486

    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
487
CodeCompletion::CodeCompletion(ModelManagerInterface *modelManager, QObject *parent)
488
    : TextEditor::ICompletionCollector(parent),
489
      m_modelManager(modelManager),
490
      m_editor(0),
491
492
      m_startPosition(0),
      m_restartCompletion(false)
493
494
495
{
    Q_ASSERT(modelManager);
}
496

Roberto Raggi's avatar
Roberto Raggi committed
497
CodeCompletion::~CodeCompletion()
498
499
{ }

Roberto Raggi's avatar
Roberto Raggi committed
500
TextEditor::ITextEditable *CodeCompletion::editor() const
501
502
{ return m_editor; }

Roberto Raggi's avatar
Roberto Raggi committed
503
int CodeCompletion::startPosition() const
504
505
{ return m_startPosition; }

Roberto Raggi's avatar
Roberto Raggi committed
506
bool CodeCompletion::shouldRestartCompletion()
507
{ return m_restartCompletion; }
508

Roberto Raggi's avatar
Roberto Raggi committed
509
bool CodeCompletion::supportsEditor(TextEditor::ITextEditable *editor)
510
{
511
    if (qobject_cast<QmlJSTextEditor *>(editor->widget()))
512
513
514
515
516
        return true;

    return false;
}

Roberto Raggi's avatar
Roberto Raggi committed
517
bool CodeCompletion::triggersCompletion(TextEditor::ITextEditable *editor)
518
519
520
521
522
523
524
525
526
527
528
529
{
    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
530
            Scanner scanner;
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
            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
547
bool CodeCompletion::maybeTriggersCompletion(TextEditor::ITextEditable *editor)
548
{
549
550
    const int cursorPosition = editor->position();
    const QChar ch = editor->characterAt(cursorPosition - 1);
551
552
553

    if (ch == QLatin1Char('(') || ch == QLatin1Char('.'))
        return true;
554
555
556
557

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

    if (isIdentifierChar(ch) && (characterUnderCursor.isSpace() ||
558
559
                                      characterUnderCursor.isNull() ||
                                      isDelimiter(characterUnderCursor))) {
560
561
562
563
564
565
566
567
568
569
570
571
        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;
572
            }
573
            return true;
574
575
        }
    }
576
577
578

    return false;
}
579

Roberto Raggi's avatar
Roberto Raggi committed
580
bool CodeCompletion::isDelimiter(const QChar &ch) const
581
582
583
584
585
586
{
    switch (ch.unicode()) {
    case '{':
    case '}':
    case '[':
    case ']':
587
    case ')':
588
    case '?':
589
    case '!':
590
591
592
593
594
595
596
597
598
599
    case ':':
    case ';':
    case ',':
        return true;

    default:
        return false;
    }
}

600
601
602
603
604
605
606
607
608
609
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;
}

610
void CodeCompletion::addCompletions(const QHash<QString, const Interpreter::Value *> &newCompletions,
611
                                    const QIcon &icon, int relevance)
612
613
614
615
616
617
618
619
{
    QHashIterator<QString, const Interpreter::Value *> it(newCompletions);
    while (it.hasNext()) {
        it.next();

        TextEditor::CompletionItem item(this);
        item.text = it.key();
        item.icon = icon;
620
        item.relevance = relevance;
621
622
623
624
625
        m_completions.append(item);
    }
}

void CodeCompletion::addCompletions(const QStringList &newCompletions,
626
                                    const QIcon &icon, int relevance)
627
628
629
630
631
{
    foreach (const QString &text, newCompletions) {
        TextEditor::CompletionItem item(this);
        item.text = text;
        item.icon = icon;
632
        item.relevance = relevance;
633
634
635
636
        m_completions.append(item);
    }
}

637
638
void CodeCompletion::addCompletionsPropertyLhs(
        const QHash<QString, const Interpreter::Value *> &newCompletions,
639
        const QIcon &icon, int relevance)
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
{
    QHashIterator<QString, const Interpreter::Value *> it(newCompletions);
    while (it.hasNext()) {
        it.next();

        TextEditor::CompletionItem item(this);
        item.text = it.key();
        if (const Interpreter::QmlObjectValue *qmlValue = dynamic_cast<const Interpreter::QmlObjectValue *>(it.value())) {
            // to distinguish "anchors." from "gradient:" we check if the right hand side
            // type is instantiatable or is the prototype of an instantiatable object
            if (qmlValue->hasChildInPackage())
                item.text.append(QLatin1String(": "));
            else
                item.text.append(QLatin1Char('.'));
        } else {
            item.text.append(QLatin1String(": "));
        }
        item.icon = icon;
658
        item.relevance = relevance;
659
660
661
662
        m_completions.append(item);
    }
}

Roberto Raggi's avatar
Roberto Raggi committed
663
int CodeCompletion::startCompletion(TextEditor::ITextEditable *editor)
664
{
665
666
    m_restartCompletion = false;

667
668
    m_editor = editor;

669
    QmlJSTextEditor *edit = qobject_cast<QmlJSTextEditor *>(m_editor->widget());
670
671
672
    if (! edit)
        return -1;

673
    m_startPosition = editor->position();
Roberto Raggi's avatar
Roberto Raggi committed
674
    const QString fileName = editor->file()->fileName();
675

Roberto Raggi's avatar
Roberto Raggi committed
676
677
    while (editor->characterAt(m_startPosition - 1).isLetterOrNumber() ||
           editor->characterAt(m_startPosition - 1) == QLatin1Char('_'))
678
        --m_startPosition;
679
680
681

    m_completions.clear();

Roberto Raggi's avatar
Cleanup    
Roberto Raggi committed
682
    const SemanticInfo semanticInfo = edit->semanticInfo();
683
    const QmlJS::Snapshot snapshot = semanticInfo.snapshot;
684
    const Document::Ptr document = semanticInfo.document;
685
686

    const QFileInfo currentFileInfo(fileName);
687

688
689
690
691
    bool isQmlFile = false;
    if (currentFileInfo.suffix() == QLatin1String("qml"))
        isQmlFile = true;

692
    const QIcon symbolIcon = iconForColor(Qt::darkCyan);
693
    const QIcon keywordIcon = iconForColor(Qt::darkYellow);
694
695

    Interpreter::Engine interp;
696
    Interpreter::Context context(&interp);
697
    Link link(&context, document, snapshot, m_modelManager->importPaths());
698

Roberto Raggi's avatar
Roberto Raggi committed
699
    // Set up the current scope chain.
700
701
    ScopeBuilder scopeBuilder(document, &context);
    scopeBuilder.push(semanticInfo.astPath(editor->position()));
702

Roberto Raggi's avatar
Roberto Raggi committed
703
    // Search for the operator that triggered the completion.
704
    QChar completionOperator;
705
    if (m_startPosition > 0)
706
        completionOperator = editor->characterAt(m_startPosition - 1);
707

708
709
710
711
712
713
714
715
    QTextCursor startPositionCursor(edit->document());
    startPositionCursor.setPosition(m_startPosition);
    CompletionContextFinder contextFinder(startPositionCursor);

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

716
717
    if (completionOperator.isSpace() || completionOperator.isNull() || isDelimiter(completionOperator) ||
            (completionOperator == QLatin1Char('(') && m_startPosition != editor->position())) {
718

Christian Kamm's avatar
Christian Kamm committed
719
720
721
722
        bool doGlobalCompletion = true;
        bool doQmlKeywordCompletion = true;
        bool doJsKeywordCompletion = true;

723
724
        if (contextFinder.isInLhsOfBinding() && qmlScopeType) {
            doGlobalCompletion = false;
Christian Kamm's avatar
Christian Kamm committed
725
726
            doJsKeywordCompletion = false;

727
728
            EnumerateProperties enumerateProperties(&context);
            enumerateProperties.setGlobalCompletion(true);
Christian Kamm's avatar
Christian Kamm committed
729
            enumerateProperties.setEnumerateGeneratedSlots(true);
730

731
732
733
734
            // id: is special
            TextEditor::CompletionItem idPropertyCompletion(this);
            idPropertyCompletion.text = QLatin1String("id: ");
            idPropertyCompletion.icon = symbolIcon;
735
            idPropertyCompletion.relevance = PropertyRelevance;
736
737
            m_completions.append(idPropertyCompletion);

738
739
            addCompletionsPropertyLhs(enumerateProperties(qmlScopeType), symbolIcon, PropertyRelevance);
            addCompletions(enumerateProperties(context.scopeChain().qmlTypes), symbolIcon, TypeRelevance);
740
741
742

            if (ScopeBuilder::isPropertyChangesObject(&context, qmlScopeType)
                    && context.scopeChain().qmlScopeObjects.size() == 2) {
743
                addCompletions(enumerateProperties(context.scopeChain().qmlScopeObjects.first()), symbolIcon, SymbolRelevance);
744
            }
745
        }
746

Christian Kamm's avatar
Christian Kamm committed
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
        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;
769
                        item.relevance = EnumValueRelevance;
Christian Kamm's avatar
Christian Kamm committed
770
771
772
773
774
775
                        m_completions.append(item);
                    }
                }
            }
        }

776
777
778
779
        if (doGlobalCompletion) {
            // It's a global completion.
            EnumerateProperties enumerateProperties(&context);
            enumerateProperties.setGlobalCompletion(true);
780
            addCompletions(enumerateProperties(), symbolIcon, SymbolRelevance);
Christian Kamm's avatar
Christian Kamm committed
781
        }
782

Christian Kamm's avatar
Christian Kamm committed
783
        if (doJsKeywordCompletion) {
784
            // add js keywords
785
            addCompletions(Scanner::keywords(), keywordIcon, KeywordRelevance);
786
787
788
        }

        // add qml extra words
Christian Kamm's avatar
Christian Kamm committed
789
        if (doQmlKeywordCompletion && isQmlFile) {
790
            static QStringList qmlWords;
791
            static QStringList qmlWordsAlsoInJs;
792
793
794

            if (qmlWords.isEmpty()) {
                qmlWords << QLatin1String("property")
795
                        //<< QLatin1String("readonly")
796
797
798
                        << QLatin1String("signal")
                        << QLatin1String("import");
            }
799
800
801
            if (qmlWordsAlsoInJs.isEmpty()) {
                qmlWordsAlsoInJs << QLatin1String("default")
                        << QLatin1String("function");
802
            }
Christian Kamm's avatar
Christian Kamm committed
803

804
            addCompletions(qmlWords, keywordIcon, KeywordRelevance);
805
            if (!doJsKeywordCompletion)
806
                addCompletions(qmlWordsAlsoInJs, keywordIcon, KeywordRelevance);
807
        }
808
809
    }

810
    else if (completionOperator == QLatin1Char('.') || completionOperator == QLatin1Char('(')) {
Roberto Raggi's avatar
Roberto Raggi committed
811
        // Look at the expression under cursor.
812
813
        QTextCursor tc = edit->textCursor();
        tc.setPosition(m_startPosition - 1);
814

815
816
        QmlExpressionUnderCursor expressionUnderCursor;
        QmlJS::AST::ExpressionNode *expression = expressionUnderCursor(tc);
817

818
        if (expression != 0 && ! isLiteral(expression)) {
819
            Evaluate evaluate(&context);
820

Roberto Raggi's avatar
Roberto Raggi committed
821
            // Evaluate the expression under cursor.
822
            const Interpreter::Value *value = interp.convertToObject(evaluate(expression));
823
824
            //qDebug() << "type:" << interp.typeId(value);

825
            if (value && completionOperator == QLatin1Char('.')) { // member completion
826
                EnumerateProperties enumerateProperties(&context);
827
                if (contextFinder.isInLhsOfBinding() && qmlScopeType && expressionUnderCursor.text().at(0).isLower())
828
                    addCompletionsPropertyLhs(enumerateProperties(value), symbolIcon, PropertyRelevance);
829
                else
830
                    addCompletions(enumerateProperties(value), symbolIcon, SymbolRelevance);
831
832
            } else if (value && completionOperator == QLatin1Char('(') && m_startPosition == editor->position()) {
                // function completion
833
                if (const Interpreter::FunctionValue *f = value->asFunctionValue()) {
834
835
                    QString functionName = expressionUnderCursor.text();
                    int indexOfDot = functionName.lastIndexOf(QLatin1Char('.'));
836
                    if (indexOfDot != -1)
837
                        functionName = functionName.mid(indexOfDot + 1);
838

839
840
841
842
                    // Recreate if necessary
                    if (!m_functionArgumentWidget)
                        m_functionArgumentWidget = new QmlJSEditor::Internal::FunctionArgumentWidget;

843
844
845
846
847
848
                    QStringList signature;
                    for (int i = 0; i < f->argumentCount(); ++i)
                        signature.append(f->argumentName(i));

                    m_functionArgumentWidget->showFunctionHint(functionName.trimmed(),
                                                               signature,
849
                                                               m_startPosition);
850
851
                }

852
                return -1; // We always return -1 when completing function prototypes.
853
854
            }
        }
855
856
857
858
859

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

        return -1;
860
861
    }

862
863
864
    if (isQmlFile && (completionOperator.isNull() || completionOperator.isSpace() || isDelimiter(completionOperator))) {
        updateSnippets();
        m_completions.append(m_snippets);
865
    }
866

867
868
869
870
    if (! m_completions.isEmpty())
        return m_startPosition;

    return -1;
871
872
}

Roberto Raggi's avatar
Roberto Raggi committed
873
void CodeCompletion::completions(QList<TextEditor::CompletionItem> *completions)
874
875
876
877
878
879
880
881
{
    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);

882
        filter(m_completions, completions, key);
883
884
885
886
887

        if (completions->size() == 1) {
            if (key == completions->first().text)
                completions->clear();
        }
888
889
890
    }
}

Roberto Raggi's avatar
Roberto Raggi committed
891
void CodeCompletion::complete(const TextEditor::CompletionItem &item)
892
{
893
894
895
896
897
    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
898
            tc.setPosition(m_startPosition, QTextCursor::KeepAnchor);
899
            toInsert = item.data.toString();
mae's avatar
mae committed
900
            edit->insertCodeSnippet(tc, toInsert);
901
902
903
904
            return;
        }
    }

905
906
907
    const int length = m_editor->position() - m_startPosition;
    m_editor->setCurPos(m_startPosition);
    m_editor->replace(length, toInsert);
908
909
910

    if (toInsert.endsWith(QLatin1Char('.')))
        m_restartCompletion = true;
911
912
}

Roberto Raggi's avatar
Roberto Raggi committed
913
bool CodeCompletion::partiallyComplete(const QList<TextEditor::CompletionItem> &completionItems)
914
915
{
    if (completionItems.count() == 1) {
916
        const TextEditor::CompletionItem item = completionItems.first();
917

918
919
920
        if (!item.data.canConvert<QString>()) {
            complete(item);
            return true;
921
922
        }
    }
923

924
    return TextEditor::ICompletionCollector::partiallyComplete(completionItems);
925
926
}

Roberto Raggi's avatar
Roberto Raggi committed
927
void CodeCompletion::cleanup()
928
929
930
931
932
933
{
    m_editor = 0;
    m_startPosition = 0;
    m_completions.clear();
}

934

Roberto Raggi's avatar
Roberto Raggi committed
935
void CodeCompletion::updateSnippets()
936
937
938
939
940
941
942
943
944
{
    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;

945
946
    const QIcon icon = iconForColor(Qt::red);

947
948
949
950
    m_snippetFileLastModified = lastModified;
    QFile file(qmlsnippets);
    file.open(QIODevice::ReadOnly);
    QXmlStreamReader xml(&file);
951
    if (xml.readNextStartElement()) {
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
        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
972
973


mae's avatar
mae committed
974
975
976
977
978
                            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
979
980
981
                            {
                                QString s = QLatin1String("<nobr>");
                                int count = 0;
mae's avatar
mae committed
982
983
984
                                for (int i = 0; i < infotip.count(); ++i) {
                                    if (infotip.at(i) != QChar::ObjectReplacementCharacter) {
                                        s += infotip.at(i);
mae's avatar
mae committed
985
986
987
988
989
                                        continue;
                                    }
                                    if (++count % 2) {
                                        s += QLatin1String("<b>");
                                    } else {
mae's avatar
mae committed
990
991
                                        if (infotip.at(i-1) == QChar::ObjectReplacementCharacter)
                                            s += QLatin1String("...");
mae's avatar
mae committed
992
993
994
                                        s += QLatin1String("</b>");
                                    }
                                }
mae's avatar
mae committed
995
                                infotip = s;
mae's avatar
mae committed
996
997
                            }

mae's avatar
mae committed
998
                            item.details = infotip;
mae's avatar
mae committed
999

1000
                            item.icon = icon;
1001
                            item.relevance = SnippetRelevance;
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
                            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;
                            }
                        }
                    }
1018
1019
                } else {
                    xml.skipCurrentElement();
1020
1021
                }
            }
1022
1023
        } else {
            xml.skipCurrentElement();
1024
1025
1026
        }
    }
    if (xml.hasError())
1027
        qWarning() << qmlsnippets << xml.errorString() << xml.lineNumber() << xml.columnNumber();
1028
1029
    file.close();
}
Roberto Raggi's avatar
Roberto Raggi committed
1030

1031
1032
static bool qmlCompletionItemLessThan(const TextEditor::CompletionItem &l, const TextEditor::CompletionItem &r)
{
1033
1034
1035
    if (l.relevance != r.relevance)
        return l.relevance > r.relevance;
    else if (l.text.isEmpty())
1036
1037
1038
        return true;
    else if (r.text.isEmpty())
        return false;
1039
1040
    else if (l.data.isValid() != r.data.isValid())
        return l.data.isValid();
1041
    else if (l.text.at(0).isUpper() && r.text.at(0).isLower())
1042
1043
1044
1045
1046
1047
1048
        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
1049
QList<TextEditor::CompletionItem> CodeCompletion::getCompletions()
Roberto Raggi's avatar
Roberto Raggi committed
1050
1051
1052
1053
1054
{
    QList<TextEditor::CompletionItem> completionItems;

    completions(&completionItems);

1055
    qStableSort(completionItems.begin(), completionItems.end(), qmlCompletionItemLessThan);
Roberto Raggi's avatar
Roberto Raggi committed
1056
1057
1058

    // Remove duplicates
    QString lastKey;
1059
    QVariant lastData;
Roberto Raggi's avatar
Roberto Raggi committed
1060
1061
1062
    QList<TextEditor::CompletionItem> uniquelist;

    foreach (const TextEditor::CompletionItem &item, completionItems) {
1063
        if (item.text != lastKey || item.data.type() != lastData.type()) {
Roberto Raggi's avatar
Roberto Raggi committed
1064
1065
            uniquelist.append(item);
            lastKey = item.text;
1066
            lastData = item.data;
Roberto Raggi's avatar
Roberto Raggi committed
1067
1068
1069
1070
1071
        }
    }

    return uniquelist;
}