qmljscodecompletion.cpp 32.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
#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

// Temporary workaround until we have proper icons for QML completion items
73
QIcon iconForColor(const QColor &color)
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
101
102
{
    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;
}

103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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();
    }
}
129

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

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

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

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

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

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

        enumerateProperties(value);

        return _properties;
    }

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

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

205
206
207
208
        return _properties;
    }

private:
209
210
211
212
213
214
215
    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);
    }

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

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

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

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

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

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

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

        _processed.insert(object);
265
        enumerateProperties(object->prototype(_context));
266

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

} // end of anonymous namespace

273
274
275
276
277
278
279
namespace QmlJSEditor {
namespace Internal {

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

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

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

    QString m_functionName;
292
    QStringList m_signature;
293
294
295
296
297
298
299
300
301
302
    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;
303
    Utils::FakeToolTip *m_popupFrame;
304
305
306
307
308
309
310
311
312
313
314
315
};


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

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

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

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

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

    if (m_currentarg != argnr) {
385
        // m_currentarg = argnr;
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
438
439
        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;
440
441
442
443
444
    prettyMethod += QLatin1Char('(');
    for (int i = 0; i < m_minimumArgumentCount; ++i) {
        if (i != 0)
            prettyMethod += QLatin1String(", ");

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

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

    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
478
CodeCompletion::CodeCompletion(ModelManagerInterface *modelManager, QObject *parent)
479
    : TextEditor::ICompletionCollector(parent),
480
      m_modelManager(modelManager),
481
      m_editor(0),
482
483
      m_startPosition(0),
      m_restartCompletion(false)
484
485
486
{
    Q_ASSERT(modelManager);
}
487

Roberto Raggi's avatar
Roberto Raggi committed
488
CodeCompletion::~CodeCompletion()
489
490
{ }

Roberto Raggi's avatar
Roberto Raggi committed
491
TextEditor::ITextEditable *CodeCompletion::editor() const
492
493
{ return m_editor; }

Roberto Raggi's avatar
Roberto Raggi committed
494
int CodeCompletion::startPosition() const
495
496
{ return m_startPosition; }

Roberto Raggi's avatar
Roberto Raggi committed
497
bool CodeCompletion::shouldRestartCompletion()
498
{ return m_restartCompletion; }
499

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

    return false;
}

Roberto Raggi's avatar
Roberto Raggi committed
508
bool CodeCompletion::triggersCompletion(TextEditor::ITextEditable *editor)
509
510
511
512
513
514
515
516
517
518
519
520
{
    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
521
            Scanner scanner;
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
            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
538
bool CodeCompletion::maybeTriggersCompletion(TextEditor::ITextEditable *editor)
539
{
540
541
    const int cursorPosition = editor->position();
    const QChar ch = editor->characterAt(cursorPosition - 1);
542
543
544

    if (ch == QLatin1Char('(') || ch == QLatin1Char('.'))
        return true;
545
546
547
548

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

    if (isIdentifierChar(ch) && (characterUnderCursor.isSpace() ||
549
550
                                      characterUnderCursor.isNull() ||
                                      isDelimiter(characterUnderCursor))) {
551
552
553
554
555
556
557
558
559
560
561
562
        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;
563
            }
564
            return true;
565
566
        }
    }
567
568
569

    return false;
}
570

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

    default:
        return false;
    }
}

591
592
593
594
595
596
597
598
599
600
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;
}

601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
void CodeCompletion::addCompletions(const QHash<QString, const Interpreter::Value *> &newCompletions,
                                    const QIcon &icon)
{
    QHashIterator<QString, const Interpreter::Value *> it(newCompletions);
    while (it.hasNext()) {
        it.next();

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

void CodeCompletion::addCompletions(const QStringList &newCompletions,
                                    const QIcon &icon)
{
    foreach (const QString &text, newCompletions) {
        TextEditor::CompletionItem item(this);
        item.text = text;
        item.icon = icon;
        m_completions.append(item);
    }
}

626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
void CodeCompletion::addCompletionsPropertyLhs(
        const QHash<QString, const Interpreter::Value *> &newCompletions,
        const QIcon &icon)
{
    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;
        m_completions.append(item);
    }
}

Roberto Raggi's avatar
Roberto Raggi committed
651
int CodeCompletion::startCompletion(TextEditor::ITextEditable *editor)
652
{
653
654
    m_restartCompletion = false;

655
656
    m_editor = editor;

657
    QmlJSTextEditor *edit = qobject_cast<QmlJSTextEditor *>(m_editor->widget());
658
659
660
    if (! edit)
        return -1;

661
    m_startPosition = editor->position();
Roberto Raggi's avatar
Roberto Raggi committed
662
    const QString fileName = editor->file()->fileName();
663

Roberto Raggi's avatar
Roberto Raggi committed
664
665
    while (editor->characterAt(m_startPosition - 1).isLetterOrNumber() ||
           editor->characterAt(m_startPosition - 1) == QLatin1Char('_'))
666
        --m_startPosition;
667
668
669

    m_completions.clear();

Roberto Raggi's avatar
Cleanup    
Roberto Raggi committed
670
    const SemanticInfo semanticInfo = edit->semanticInfo();
671
    const QmlJS::Snapshot snapshot = semanticInfo.snapshot;
672
    const Document::Ptr document = semanticInfo.document;
673
674

    const QFileInfo currentFileInfo(fileName);
675

676
677
678
679
    bool isQmlFile = false;
    if (currentFileInfo.suffix() == QLatin1String("qml"))
        isQmlFile = true;

680
    const QIcon symbolIcon = iconForColor(Qt::darkCyan);
681
    const QIcon keywordIcon = iconForColor(Qt::darkYellow);
682
683

    Interpreter::Engine interp;
684
    Interpreter::Context context(&interp);
685
    Link link(&context, document, snapshot, m_modelManager->importPaths());
686

Roberto Raggi's avatar
Roberto Raggi committed
687
    // Set up the current scope chain.
688
689
    ScopeBuilder scopeBuilder(document, &context);
    scopeBuilder.push(semanticInfo.astPath(editor->position()));
690

Roberto Raggi's avatar
Roberto Raggi committed
691
    // Search for the operator that triggered the completion.
692
    QChar completionOperator;
693
    if (m_startPosition > 0)
694
        completionOperator = editor->characterAt(m_startPosition - 1);
695

696
697
698
699
700
701
702
703
    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());

704
705
    if (completionOperator.isSpace() || completionOperator.isNull() || isDelimiter(completionOperator) ||
            (completionOperator == QLatin1Char('(') && m_startPosition != editor->position())) {
706

Christian Kamm's avatar
Christian Kamm committed
707
708
709
710
        bool doGlobalCompletion = true;
        bool doQmlKeywordCompletion = true;
        bool doJsKeywordCompletion = true;

711
712
        if (contextFinder.isInLhsOfBinding() && qmlScopeType) {
            doGlobalCompletion = false;
Christian Kamm's avatar
Christian Kamm committed
713
714
            doJsKeywordCompletion = false;

715
716
            EnumerateProperties enumerateProperties(&context);
            enumerateProperties.setGlobalCompletion(true);
Christian Kamm's avatar
Christian Kamm committed
717
            enumerateProperties.setEnumerateGeneratedSlots(true);
718

719
720
721
722
723
724
            // id: is special
            TextEditor::CompletionItem idPropertyCompletion(this);
            idPropertyCompletion.text = QLatin1String("id: ");
            idPropertyCompletion.icon = symbolIcon;
            m_completions.append(idPropertyCompletion);

725
            addCompletionsPropertyLhs(enumerateProperties(qmlScopeType), symbolIcon);
726
            addCompletions(enumerateProperties(context.scopeChain().qmlTypes), symbolIcon);
727
728
729
730
731

            if (ScopeBuilder::isPropertyChangesObject(&context, qmlScopeType)
                    && context.scopeChain().qmlScopeObjects.size() == 2) {
                addCompletions(enumerateProperties(context.scopeChain().qmlScopeObjects.first()), symbolIcon);
            }
732
        }
733

Christian Kamm's avatar
Christian Kamm committed
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
        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);
                    }
                }
            }
        }

762
763
764
765
        if (doGlobalCompletion) {
            // It's a global completion.
            EnumerateProperties enumerateProperties(&context);
            enumerateProperties.setGlobalCompletion(true);
766
            addCompletions(enumerateProperties(), symbolIcon);
Christian Kamm's avatar
Christian Kamm committed
767
        }
768

Christian Kamm's avatar
Christian Kamm committed
769
        if (doJsKeywordCompletion) {
770
            // add js keywords
771
            addCompletions(Scanner::keywords(), keywordIcon);
772
773
774
        }

        // add qml extra words
Christian Kamm's avatar
Christian Kamm committed
775
        if (doQmlKeywordCompletion && isQmlFile) {
776
            static QStringList qmlWords;
777
            static QStringList qmlWordsAlsoInJs;
778
779
780

            if (qmlWords.isEmpty()) {
                qmlWords << QLatin1String("property")
781
                        //<< QLatin1String("readonly")
782
783
784
                        << QLatin1String("signal")
                        << QLatin1String("import");
            }
785
786
787
            if (qmlWordsAlsoInJs.isEmpty()) {
                qmlWordsAlsoInJs << QLatin1String("default")
                        << QLatin1String("function");
788
            }
Christian Kamm's avatar
Christian Kamm committed
789

790
791
792
            addCompletions(qmlWords, keywordIcon);
            if (!doJsKeywordCompletion)
                addCompletions(qmlWordsAlsoInJs, keywordIcon);
793
        }
794
795
    }

796
    else if (completionOperator == QLatin1Char('.') || completionOperator == QLatin1Char('(')) {
Roberto Raggi's avatar
Roberto Raggi committed
797
        // Look at the expression under cursor.
798
799
        QTextCursor tc = edit->textCursor();
        tc.setPosition(m_startPosition - 1);
800

801
802
        QmlExpressionUnderCursor expressionUnderCursor;
        QmlJS::AST::ExpressionNode *expression = expressionUnderCursor(tc);
803

804
        if (expression != 0 && ! isLiteral(expression)) {
805
            Evaluate evaluate(&context);
806

Roberto Raggi's avatar
Roberto Raggi committed
807
            // Evaluate the expression under cursor.
808
            const Interpreter::Value *value = interp.convertToObject(evaluate(expression));
809
810
            //qDebug() << "type:" << interp.typeId(value);

811
            if (value && completionOperator == QLatin1Char('.')) { // member completion
812
                EnumerateProperties enumerateProperties(&context);
813
                if (contextFinder.isInLhsOfBinding() && qmlScopeType && expressionUnderCursor.text().at(0).isLower())
814
815
816
                    addCompletionsPropertyLhs(enumerateProperties(value), symbolIcon);
                else
                    addCompletions(enumerateProperties(value), symbolIcon);
817
818
            } else if (value && completionOperator == QLatin1Char('(') && m_startPosition == editor->position()) {
                // function completion
819
                if (const Interpreter::FunctionValue *f = value->asFunctionValue()) {
820
821
                    QString functionName = expressionUnderCursor.text();
                    int indexOfDot = functionName.lastIndexOf(QLatin1Char('.'));
822
                    if (indexOfDot != -1)
823
                        functionName = functionName.mid(indexOfDot + 1);
824

825
826
827
828
                    // Recreate if necessary
                    if (!m_functionArgumentWidget)
                        m_functionArgumentWidget = new QmlJSEditor::Internal::FunctionArgumentWidget;

829
830
831
832
833
834
                    QStringList signature;
                    for (int i = 0; i < f->argumentCount(); ++i)
                        signature.append(f->argumentName(i));

                    m_functionArgumentWidget->showFunctionHint(functionName.trimmed(),
                                                               signature,
835
                                                               m_startPosition);
836
837
                }

838
                return -1; // We always return -1 when completing function prototypes.
839
840
            }
        }
841
842
843
844
845

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

        return -1;
846
847
    }

848
849
850
    if (isQmlFile && (completionOperator.isNull() || completionOperator.isSpace() || isDelimiter(completionOperator))) {
        updateSnippets();
        m_completions.append(m_snippets);
851
    }
852

853
854
855
856
    if (! m_completions.isEmpty())
        return m_startPosition;

    return -1;
857
858
}

Roberto Raggi's avatar
Roberto Raggi committed
859
void CodeCompletion::completions(QList<TextEditor::CompletionItem> *completions)
860
861
862
863
864
865
866
867
{
    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);

868
        filter(m_completions, completions, key);
869
870
871
872
873

        if (completions->size() == 1) {
            if (key == completions->first().text)
                completions->clear();
        }
874
875
876
    }
}

Roberto Raggi's avatar
Roberto Raggi committed
877
void CodeCompletion::complete(const TextEditor::CompletionItem &item)
878
{
879
880
881
882
883
    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
884
            tc.setPosition(m_startPosition, QTextCursor::KeepAnchor);
885
            toInsert = item.data.toString();
mae's avatar
mae committed
886
            edit->insertCodeSnippet(tc, toInsert);
887
888
889
890
            return;
        }
    }

891
892
893
    const int length = m_editor->position() - m_startPosition;
    m_editor->setCurPos(m_startPosition);
    m_editor->replace(length, toInsert);
894
895
896

    if (toInsert.endsWith(QLatin1Char('.')))
        m_restartCompletion = true;
897
898
}

Roberto Raggi's avatar
Roberto Raggi committed
899
bool CodeCompletion::partiallyComplete(const QList<TextEditor::CompletionItem> &completionItems)
900
901
{
    if (completionItems.count() == 1) {
902
        const TextEditor::CompletionItem item = completionItems.first();
903

904
905
906
        if (!item.data.canConvert<QString>()) {
            complete(item);
            return true;
907
908
        }
    }
909

910
    return TextEditor::ICompletionCollector::partiallyComplete(completionItems);
911
912
}

Roberto Raggi's avatar
Roberto Raggi committed
913
void CodeCompletion::cleanup()
914
915
916
917
918
919
{
    m_editor = 0;
    m_startPosition = 0;
    m_completions.clear();
}

920

Roberto Raggi's avatar
Roberto Raggi committed
921
void CodeCompletion::updateSnippets()
922
923
924
925
926
927
928
929
930
{
    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;

931
932
    const QIcon icon = iconForColor(Qt::red);

933
934
935
936
    m_snippetFileLastModified = lastModified;
    QFile file(qmlsnippets);
    file.open(QIODevice::ReadOnly);
    QXmlStreamReader xml(&file);
937
    if (xml.readNextStartElement()) {
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
        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
958
959


mae's avatar
mae committed
960
961
962
963
964
                            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
965
966
967
                            {
                                QString s = QLatin1String("<nobr>");
                                int count = 0;
mae's avatar
mae committed
968
969
970
                                for (int i = 0; i < infotip.count(); ++i) {
                                    if (infotip.at(i) != QChar::ObjectReplacementCharacter) {
                                        s += infotip.at(i);
mae's avatar
mae committed
971
972
973
974
975
                                        continue;
                                    }
                                    if (++count % 2) {
                                        s += QLatin1String("<b>");
                                    } else {
mae's avatar
mae committed
976
977
                                        if (infotip.at(i-1) == QChar::ObjectReplacementCharacter)
                                            s += QLatin1String("...");
mae's avatar
mae committed
978
979
980
                                        s += QLatin1String("</b>");
                                    }
                                }
mae's avatar
mae committed
981
                                infotip = s;
mae's avatar
mae committed
982
983
                            }

mae's avatar
mae committed
984
                            item.details = infotip;
mae's avatar
mae committed
985

986
                            item.icon = icon;
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
                            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;
                            }
                        }
                    }
1003
1004
                } else {
                    xml.skipCurrentElement();
1005
1006
                }
            }
1007
1008
        } else {
            xml.skipCurrentElement();
1009
1010
1011
        }
    }
    if (xml.hasError())
1012
        qWarning() << qmlsnippets << xml.errorString() << xml.lineNumber() << xml.columnNumber();
1013
1014
    file.close();
}
Roberto Raggi's avatar
Roberto Raggi committed
1015

1016
1017
static bool qmlCompletionItemLessThan(const TextEditor::CompletionItem &l, const TextEditor::CompletionItem &r)
{
1018
1019
1020
1021
    if (l.text.isEmpty())
        return true;
    else if (r.text.isEmpty())
        return false;
1022
1023
    else if (l.data.isValid() != r.data.isValid())
        return l.data.isValid();
1024
    else if (l.text.at(0).isUpper() && r.text.at(0).isLower())
1025
1026
1027
1028
1029
1030
1031
        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
1032
QList<TextEditor::CompletionItem> CodeCompletion::getCompletions()
Roberto Raggi's avatar
Roberto Raggi committed
1033
1034
1035
1036
1037
{
    QList<TextEditor::CompletionItem> completionItems;

    completions(&completionItems);

1038
    qStableSort(completionItems.begin(), completionItems.end(), qmlCompletionItemLessThan);
Roberto Raggi's avatar
Roberto Raggi committed
1039
1040
1041

    // Remove duplicates
    QString lastKey;
1042
    QVariant lastData;
Roberto Raggi's avatar
Roberto Raggi committed
1043
1044
1045
    QList<TextEditor::CompletionItem> uniquelist;

    foreach (const TextEditor::CompletionItem &item, completionItems) {
1046
        if (item.text != lastKey || item.data.type() != lastData.type()) {
Roberto Raggi's avatar
Roberto Raggi committed
1047
1048
            uniquelist.append(item);
            lastKey = item.text;
1049
            lastData = item.data;
Roberto Raggi's avatar
Roberto Raggi committed
1050
1051
1052
1053
1054
        }
    }

    return uniquelist;
}