qmlcodecompletion.cpp 25.6 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**************************************************************************
**
** This file is part of Qt Creator
**
** Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies).
**
** 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.
**
**************************************************************************/

30
#include "qmlcodecompletion.h"
31
#include "qmlexpressionundercursor.h"
32
#include "qmljseditor.h"
33
34
#include "qmlmodelmanagerinterface.h"

35
#include <qmljs/parser/qmljsast_p.h>
36
#include <qmljs/qmljsbind.h>
37
#include <qmljs/qmljslink.h>
38
#include <qmljs/qmljsinterpreter.h>
39
#include <qmljs/qmljsscanner.h>
40
#include <qmljs/qmljscheck.h>
41

42
#include <texteditor/basetexteditor.h>
43

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

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

53
54
55
56
57
58
59
60
#include <QtGui/QPainter>
#include <QtGui/QLabel>
#include <QtGui/QStylePainter>
#include <QtGui/QStyleOption>
#include <QtGui/QToolButton>
#include <QtGui/QHBoxLayout>
#include <QtGui/QApplication>
#include <QtGui/QDesktopWidget>
61

62
63
using namespace QmlJSEditor;
using namespace QmlJSEditor::Internal;
64
using namespace QmlJS;
65

66
67
68
69
70
71
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

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

98
namespace {
99

100
101
102
103
104
class SearchPropertyDefinitions: protected AST::Visitor
{
    QList<AST::UiPublicMember *> _properties;

public:
105
    QList<AST::UiPublicMember *> operator()(Document::Ptr doc)
106
107
    {
        _properties.clear();
108
109
        if (doc && doc->qmlProgram())
            doc->qmlProgram()->accept(this);
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
        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
127
class EnumerateProperties: private Interpreter::MemberProcessor
128
129
130
{
    QSet<const Interpreter::ObjectValue *> _processed;
    QHash<QString, const Interpreter::Value *> _properties;
131
    bool _globalCompletion;
132
133

public:
134
135
136
137
138
139
140
141
142
143
    EnumerateProperties()
        : _globalCompletion(false)
    {
    }

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

Roberto Raggi's avatar
Roberto Raggi committed
144
145
    QHash<QString, const Interpreter::Value *> operator()(const Interpreter::Value *value,
                                                          bool lookAtScope = false)
146
147
148
    {
        _processed.clear();
        _properties.clear();
Roberto Raggi's avatar
Roberto Raggi committed
149
        enumerateProperties(value, lookAtScope);
150
151
152
153
        return _properties;
    }

private:
154
    virtual bool processProperty(const QString &name, const Interpreter::Value *value)
Roberto Raggi's avatar
Cleanup    
Roberto Raggi committed
155
156
157
158
159
    {
        _properties.insert(name, value);
        return true;
    }

160
161
162
163
164
165
166
    virtual bool processEnumerator(const QString &name, const Interpreter::Value *value)
    {
        if (! _globalCompletion)
            _properties.insert(name, value);
        return true;
    }

167
    virtual bool processSignal(const QString &, const Interpreter::Value *)
168
169
170
171
    {
        return true;
    }

172
    virtual bool processSlot(const QString &name, const Interpreter::Value *value)
173
174
175
176
177
178
179
    {
        if (! _globalCompletion)
            _properties.insert(name, value);
        return true;
    }

    virtual bool processGeneratedSlot(const QString &name, const Interpreter::Value *value)
180
181
182
183
184
185
    {
        if (_globalCompletion)
            _properties.insert(name, value);
        return true;
    }

Roberto Raggi's avatar
Roberto Raggi committed
186
    void enumerateProperties(const Interpreter::Value *value, bool lookAtScope)
187
188
189
190
    {
        if (! value)
            return;
        else if (const Interpreter::ObjectValue *object = value->asObjectValue()) {
Roberto Raggi's avatar
Roberto Raggi committed
191
            enumerateProperties(object, lookAtScope);
192
193
194
        }
    }

Roberto Raggi's avatar
Roberto Raggi committed
195
    void enumerateProperties(const Interpreter::ObjectValue *object, bool lookAtScope)
196
197
198
199
200
    {
        if (! object || _processed.contains(object))
            return;

        _processed.insert(object);
Roberto Raggi's avatar
Roberto Raggi committed
201
202
203
204
        enumerateProperties(object->prototype(), /* lookAtScope = */ false);

        if (lookAtScope)
            enumerateProperties(object->scope(), /* lookAtScope = */ true);
205

Roberto Raggi's avatar
Cleanup    
Roberto Raggi committed
206
        object->processMembers(this);
207
208
209
210
211
    }
};

} // end of anonymous namespace

212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
namespace QmlJSEditor {
namespace Internal {

class FakeToolTipFrame : public QWidget
{
public:
    FakeToolTipFrame(QWidget *parent = 0) :
        QWidget(parent, Qt::ToolTip | Qt::WindowStaysOnTopHint)
    {
        setFocusPolicy(Qt::NoFocus);
        setAttribute(Qt::WA_DeleteOnClose);

        // Set the window and button text to the tooltip text color, since this
        // widget draws the background as a tooltip.
        QPalette p = palette();
        const QColor toolTipTextColor = p.color(QPalette::Inactive, QPalette::ToolTipText);
        p.setColor(QPalette::Inactive, QPalette::WindowText, toolTipTextColor);
        p.setColor(QPalette::Inactive, QPalette::ButtonText, toolTipTextColor);
        setPalette(p);
    }

protected:
    void paintEvent(QPaintEvent *e);
    void resizeEvent(QResizeEvent *e);
};

class FunctionArgumentWidget : public QLabel
{
public:
    FunctionArgumentWidget();
242
243
244
    void showFunctionHint(const QString &functionName,
                          const QStringList &signature,
                          int startPosition);
245
246
247
248
249
250
251
252
253

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

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

    QString m_functionName;
254
    QStringList m_signature;
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
    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;
    FakeToolTipFrame *m_popupFrame;
};

void FakeToolTipFrame::paintEvent(QPaintEvent *)
{
    QStylePainter p(this);
    QStyleOptionFrame opt;
    opt.init(this);
    p.drawPrimitive(QStyle::PE_PanelTipLabel, opt);
    p.end();
}

void FakeToolTipFrame::resizeEvent(QResizeEvent *)
{
    QStyleHintReturnMask frameMask;
    QStyleOption option;
    option.init(this);
    if (style()->styleHint(QStyle::SH_ToolTip_Mask, &option, this, &frameMask))
        setMask(frameMask.region);
}


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

    m_popupFrame = new FakeToolTipFrame(m_editor->widget());

    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);
    setMargin(1);

    qApp->installEventFilter(this);
}

321
void FunctionArgumentWidget::showFunctionHint(const QString &functionName, const QStringList &signature, int startPosition)
322
323
324
325
326
{
    if (m_startpos == startPosition)
        return;

    m_functionName = functionName;
327
328
    m_signature = signature;
    m_minimumArgumentCount = signature.size();
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
    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;
354
355
    QmlJSScanner tokenize;
    const QList<Token> tokens = tokenize(str);
356
    for (int i = 0; i < tokens.count(); ++i) {
357
358
        const Token &tk = tokens.at(i);
        if (tk.is(Token::LeftParenthesis))
359
            ++parcount;
360
        else if (tk.is(Token::RightParenthesis))
361
            --parcount;
362
        else if (! parcount && tk.is(Token::Colon))
363
364
365
366
            ++argnr;
    }

    if (m_currentarg != argnr) {
367
        // m_currentarg = argnr;
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
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
        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;
422
423
424
425
426
427
428
429
430
431
432
    prettyMethod += QLatin1Char('(');
    for (int i = 0; i < m_minimumArgumentCount; ++i) {
        if (i != 0)
            prettyMethod += QLatin1String(", ");

        prettyMethod += QLatin1String("arg");

        if (m_minimumArgumentCount != 1)
            prettyMethod += QString::number(i + 1);
    }
    prettyMethod += QLatin1Char(')');
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456

    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
457
QmlCodeCompletion::QmlCodeCompletion(QmlModelManagerInterface *modelManager, QObject *parent)
458
    : TextEditor::ICompletionCollector(parent),
459
      m_modelManager(modelManager),
460
461
      m_editor(0),
      m_startPosition(0),
Roberto Raggi's avatar
Roberto Raggi committed
462
      m_caseSensitivity(Qt::CaseSensitive)
463
464
465
{
    Q_ASSERT(modelManager);
}
466

467
QmlCodeCompletion::~QmlCodeCompletion()
468
469
{ }

470
Qt::CaseSensitivity QmlCodeCompletion::caseSensitivity() const
471
472
{ return m_caseSensitivity; }

473
void QmlCodeCompletion::setCaseSensitivity(Qt::CaseSensitivity caseSensitivity)
474
475
{ m_caseSensitivity = caseSensitivity; }

476
477
478
479
480
481
TextEditor::ITextEditable *QmlCodeCompletion::editor() const
{ return m_editor; }

int QmlCodeCompletion::startPosition() const
{ return m_startPosition; }

482
483
484
bool QmlCodeCompletion::shouldRestartCompletion()
{ return false; }

485
bool QmlCodeCompletion::supportsEditor(TextEditor::ITextEditable *editor)
486
{
487
    if (qobject_cast<QmlJSTextEditor *>(editor->widget()))
488
489
490
491
492
        return true;

    return false;
}

493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
static 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();
    }
}

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

    default:
        return ch.isLetterOrNumber();
    }
}

520
521
bool QmlCodeCompletion::triggersCompletion(TextEditor::ITextEditable *editor)
{
522
523
    const int cursorPosition = editor->position();
    const QChar ch = editor->characterAt(cursorPosition - 1);
524
525
526

    if (ch == QLatin1Char('(') || ch == QLatin1Char('.'))
        return true;
527
    else if (isIdentifierChar(ch)) {
528
529
530
531
532
533
534
535
536
537
538
539
        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;
540
            }
541
            return true;
542
543
        }
    }
544
545
546

    return false;
}
547

548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
bool QmlCodeCompletion::isDelimiter(const QChar &ch) const
{
    switch (ch.unicode()) {
    case '{':
    case '}':
    case '[':
    case ']':
    case '?':
    case ':':
    case ';':
    case ',':
        return true;

    default:
        return false;
    }
}

566
int QmlCodeCompletion::startCompletion(TextEditor::ITextEditable *editor)
567
568
569
{
    m_editor = editor;

570
    QmlJSTextEditor *edit = qobject_cast<QmlJSTextEditor *>(m_editor->widget());
571
572
573
    if (! edit)
        return -1;

574
    m_startPosition = editor->position();
Roberto Raggi's avatar
Roberto Raggi committed
575
    const QString fileName = editor->file()->fileName();
576

Roberto Raggi's avatar
Roberto Raggi committed
577
578
    while (editor->characterAt(m_startPosition - 1).isLetterOrNumber() ||
           editor->characterAt(m_startPosition - 1) == QLatin1Char('_'))
579
        --m_startPosition;
580
581
582

    m_completions.clear();

Roberto Raggi's avatar
Roberto Raggi committed
583
    QmlJS::Snapshot snapshot = m_modelManager->snapshot();
584

585
586
    SemanticInfo semanticInfo = edit->semanticInfo();
    Document::Ptr qmlDocument = semanticInfo.document;
587
588
    if (qmlDocument.isNull())
        return -1;
589
590

    const QFileInfo currentFileInfo(fileName);
591

592
593
594
595
    bool isQmlFile = false;
    if (currentFileInfo.suffix() == QLatin1String("qml"))
        isQmlFile = true;

596
597
598
599
    const QIcon symbolIcon = iconForColor(Qt::darkCyan);

    Interpreter::Engine interp;

Roberto Raggi's avatar
Roberto Raggi committed
600
    // Set up the current scope chain.
601
602
603
604
    Interpreter::ObjectValue *scope = interp.globalObject();

    if (isQmlFile) {
        AST::UiObjectMember *declaringMember = 0;
Roberto Raggi's avatar
Roberto Raggi committed
605

606
        const int cursorPosition = editor->position();
607
608
609
610
611
        for (int i = semanticInfo.ranges.size() - 1; i != -1; --i) {
            const Range &range = semanticInfo.ranges.at(i);
            if (range.begin.isNull() || range.end.isNull()) {
                continue;
            } else if (cursorPosition >= range.begin.position() && cursorPosition <= range.end.position()) {
612
                declaringMember = range.ast;
613
                break;
614
            }
615
        }
Roberto Raggi's avatar
Roberto Raggi committed
616

617
        scope = Bind::scopeChainAt(qmlDocument, snapshot, &interp, declaringMember);
618
    }
619

Roberto Raggi's avatar
Roberto Raggi committed
620
    // Search for the operator that triggered the completion.
621
    QChar completionOperator;
622
    if (m_startPosition > 0)
623
        completionOperator = editor->characterAt(m_startPosition - 1);
624

625
626
    if (completionOperator.isSpace() || completionOperator.isNull() || isDelimiter(completionOperator) ||
            (completionOperator == QLatin1Char('(') && m_startPosition != editor->position())) {
627
        // It's a global completion.
628
        EnumerateProperties enumerateProperties;
629
        enumerateProperties.setGlobalCompletion(true);
Roberto Raggi's avatar
Roberto Raggi committed
630
        QHashIterator<QString, const Interpreter::Value *> it(enumerateProperties(scope, /* lookAtScope = */ true));
631
632
        while (it.hasNext()) {
            it.next();
633

634
            TextEditor::CompletionItem item(this);
635
636
            item.text = it.key();
            item.icon = symbolIcon;
637
638
            m_completions.append(item);
        }
639
640
    }

641
    else if (completionOperator == QLatin1Char('.') || completionOperator == QLatin1Char('(')) {
Roberto Raggi's avatar
Roberto Raggi committed
642
        // Look at the expression under cursor.
643
644
        QTextCursor tc = edit->textCursor();
        tc.setPosition(m_startPosition - 1);
645

646
647
        QmlExpressionUnderCursor expressionUnderCursor;
        QmlJS::AST::ExpressionNode *expression = expressionUnderCursor(tc);
648
649
        //qDebug() << "expression:" << expression;

650
        if (expression  != 0) {
651
            Check evaluate(&interp);
652

Roberto Raggi's avatar
Roberto Raggi committed
653
            // Evaluate the expression under cursor.
654
            const Interpreter::Value *value = interp.convertToObject(evaluate(expression , scope));
655
656
            //qDebug() << "type:" << interp.typeId(value);

657
658
659
660
661
662
663
664
665
666
667
            if (value && completionOperator == QLatin1Char('.')) { // member completion
                EnumerateProperties enumerateProperties;
                QHashIterator<QString, const Interpreter::Value *> it(enumerateProperties(value));
                while (it.hasNext()) {
                    it.next();

                    TextEditor::CompletionItem item(this);
                    item.text = it.key();
                    item.icon = symbolIcon;
                    m_completions.append(item);
                }
668
669
            } else if (value && completionOperator == QLatin1Char('(') && m_startPosition == editor->position()) {
                // function completion
670
                if (const Interpreter::FunctionValue *f = value->asFunctionValue()) {
671
672
                    QString functionName = expressionUnderCursor.text();
                    int indexOfDot = functionName.lastIndexOf(QLatin1Char('.'));
673
                    if (indexOfDot != -1)
674
                        functionName = functionName.mid(indexOfDot + 1);
675

676
677
678
679
                    // Recreate if necessary
                    if (!m_functionArgumentWidget)
                        m_functionArgumentWidget = new QmlJSEditor::Internal::FunctionArgumentWidget;

680
681
682
683
684
685
                    QStringList signature;
                    for (int i = 0; i < f->argumentCount(); ++i)
                        signature.append(f->argumentName(i));

                    m_functionArgumentWidget->showFunctionHint(functionName.trimmed(),
                                                               signature,
686
                                                               m_startPosition);
687
688
                }

689
                return -1; // We always return -1 when completing function prototypes.
690
691
            }
        }
692
693
694
695
696

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

        return -1;
697
698
    }

699
700
701
    if (isQmlFile && (completionOperator.isNull() || completionOperator.isSpace() || isDelimiter(completionOperator))) {
        updateSnippets();
        m_completions.append(m_snippets);
702
    }
703

704
705
706
707
    if (! m_completions.isEmpty())
        return m_startPosition;

    return -1;
708
709
}

710
void QmlCodeCompletion::completions(QList<TextEditor::CompletionItem> *completions)
711
712
713
714
715
716
717
718
{
    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);

719
        filter(m_completions, completions, key, FirstLetterCaseSensitive);
720
721
722
    }
}

723
void QmlCodeCompletion::complete(const TextEditor::CompletionItem &item)
724
{
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
    QString toInsert = item.text;

    if (QmlJSTextEditor *edit = qobject_cast<QmlJSTextEditor *>(m_editor->widget())) {
        if (item.data.isValid()) {
            QTextCursor tc = edit->textCursor();
            tc.beginEditBlock();
            tc.setPosition(m_startPosition);
            tc.setPosition(m_editor->position(), QTextCursor::KeepAnchor);
            tc.removeSelectedText();

            toInsert = item.data.toString();
            edit->insertCodeSnippet(toInsert);
            tc.endEditBlock();
            return;
        }
    }

742
743
744
745
746
    const int length = m_editor->position() - m_startPosition;
    m_editor->setCurPos(m_startPosition);
    m_editor->replace(length, toInsert);
}

747
bool QmlCodeCompletion::partiallyComplete(const QList<TextEditor::CompletionItem> &completionItems)
748
749
{
    if (completionItems.count() == 1) {
750
        const TextEditor::CompletionItem item = completionItems.first();
751

752
753
754
        if (!item.data.canConvert<QString>()) {
            complete(item);
            return true;
755
756
        }
    }
757

758
    return TextEditor::ICompletionCollector::partiallyComplete(completionItems);
759
760
}

761
void QmlCodeCompletion::cleanup()
762
763
764
765
766
767
{
    m_editor = 0;
    m_startPosition = 0;
    m_completions.clear();
}

768
769
770
771
772
773
774
775
776
777
778

void QmlCodeCompletion::updateSnippets()
{
    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;

779
780
    const QIcon icon = iconForColor(Qt::red);

781
782
783
784
    m_snippetFileLastModified = lastModified;
    QFile file(qmlsnippets);
    file.open(QIODevice::ReadOnly);
    QXmlStreamReader xml(&file);
785
    if (xml.readNextStartElement()) {
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
        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);
806
                            item.icon = icon;
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
                            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;
                            }
                        }
                    }
823
824
                } else {
                    xml.skipCurrentElement();
825
826
                }
            }
827
828
        } else {
            xml.skipCurrentElement();
829
830
831
        }
    }
    if (xml.hasError())
832
        qWarning() << qmlsnippets << xml.errorString() << xml.lineNumber() << xml.columnNumber();
833
834
    file.close();
}
Roberto Raggi's avatar
Roberto Raggi committed
835

836
837
838
839
840
841
842
843
844
845
static bool qmlCompletionItemLessThan(const TextEditor::CompletionItem &l, const TextEditor::CompletionItem &r)
{
    if (l.text.at(0).isUpper() && r.text.at(0).isLower())
        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
846
847
848
849
850
851
QList<TextEditor::CompletionItem> QmlCodeCompletion::getCompletions()
{
    QList<TextEditor::CompletionItem> completionItems;

    completions(&completionItems);

852
    qStableSort(completionItems.begin(), completionItems.end(), qmlCompletionItemLessThan);
Roberto Raggi's avatar
Roberto Raggi committed
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869

    // Remove duplicates
    QString lastKey;
    QList<TextEditor::CompletionItem> uniquelist;

    foreach (const TextEditor::CompletionItem &item, completionItems) {
        if (item.text != lastKey) {
            uniquelist.append(item);
            lastKey = item.text;
        } else {
            if (item.data.canConvert<QString>())
                uniquelist.append(item);
        }
    }

    return uniquelist;
}