qmljscompletionassist.cpp 36.5 KB
Newer Older
hjk's avatar
hjk committed
1
/****************************************************************************
Leandro Melo's avatar
Leandro Melo committed
2
**
3
** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies).
hjk's avatar
hjk committed
4
** Contact: http://www.qt-project.org/legal
Leandro Melo's avatar
Leandro Melo committed
5
**
hjk's avatar
hjk committed
6
** This file is part of Qt Creator.
Leandro Melo's avatar
Leandro Melo committed
7
**
hjk's avatar
hjk committed
8 9 10 11 12 13 14
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Digia.  For licensing terms and
** conditions see http://qt.digia.com/licensing.  For further information
** use the contact form at http://qt.digia.com/contact-us.
Leandro Melo's avatar
Leandro Melo committed
15 16
**
** GNU Lesser General Public License Usage
hjk's avatar
hjk committed
17 18 19 20 21 22 23 24 25
** 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.
**
** In addition, as a special exception, Digia gives you certain additional
** rights.  These rights are described in the Digia Qt LGPL Exception
Leandro Melo's avatar
Leandro Melo committed
26 27
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
hjk's avatar
hjk committed
28
****************************************************************************/
Leandro Melo's avatar
Leandro Melo committed
29 30 31 32 33 34

#include "qmljscompletionassist.h"
#include "qmljseditorconstants.h"
#include "qmljsreuse.h"
#include "qmlexpressionundercursor.h"

35
#include <coreplugin/idocument.h>
Leandro Melo's avatar
Leandro Melo committed
36

37
#include <texteditor/codeassist/assistinterface.h>
Leandro Melo's avatar
Leandro Melo committed
38 39 40
#include <texteditor/codeassist/genericproposal.h>
#include <texteditor/codeassist/functionhintproposal.h>
#include <texteditor/codeassist/ifunctionhintproposalmodel.h>
41 42
#include <texteditor/texteditorsettings.h>
#include <texteditor/completionsettings.h>
Leandro Melo's avatar
Leandro Melo committed
43 44 45 46 47 48

#include <utils/qtcassert.h>

#include <qmljs/qmljsmodelmanagerinterface.h>
#include <qmljs/parser/qmljsast_p.h>
#include <qmljs/qmljsinterpreter.h>
49
#include <qmljs/qmljscontext.h>
50
#include <qmljs/qmljsscopechain.h>
Leandro Melo's avatar
Leandro Melo committed
51 52 53
#include <qmljs/qmljsscanner.h>
#include <qmljs/qmljsbind.h>
#include <qmljs/qmljscompletioncontextfinder.h>
54
#include <qmljs/qmljsbundle.h>
Leandro Melo's avatar
Leandro Melo committed
55
#include <qmljs/qmljsscopebuilder.h>
56
#include <projectexplorer/projectexplorer.h>
Leandro Melo's avatar
Leandro Melo committed
57

58 59 60 61 62 63
#include <QFile>
#include <QFileInfo>
#include <QDir>
#include <QDirIterator>
#include <QStringList>
#include <QIcon>
64
#include <QTextDocumentFragment>
Leandro Melo's avatar
Leandro Melo committed
65 66

using namespace QmlJS;
67
using namespace QmlJSTools;
Leandro Melo's avatar
Leandro Melo committed
68 69
using namespace TextEditor;

70 71 72 73
namespace QmlJSEditor {

using namespace Internal;

Leandro Melo's avatar
Leandro Melo committed
74 75 76 77 78 79 80 81 82 83 84
namespace {

enum CompletionOrder {
    EnumValueOrder = -5,
    SnippetOrder = -15,
    PropertyOrder = -10,
    SymbolOrder = -20,
    KeywordOrder = -25,
    TypeOrder = -30
};

85
static void addCompletion(QList<AssistProposalItem *> *completions,
86 87 88 89 90 91 92 93
                          const QString &text,
                          const QIcon &icon,
                          int order,
                          const QVariant &data = QVariant())
{
    if (text.isEmpty())
        return;

94
    AssistProposalItem *item = new QmlJSAssistProposalItem;
95 96 97 98 99 100 101
    item->setText(text);
    item->setIcon(icon);
    item->setOrder(order);
    item->setData(data);
    completions->append(item);
}

102
static void addCompletions(QList<AssistProposalItem *> *completions,
103 104 105 106 107 108 109 110 111 112 113 114 115 116
                           const QStringList &newCompletions,
                           const QIcon &icon,
                           int order)
{
    foreach (const QString &text, newCompletions)
        addCompletion(completions, text, icon, order);
}

class PropertyProcessor
{
public:
    virtual void operator()(const Value *base, const QString &name, const Value *value) = 0;
};

117 118 119 120 121 122 123
class CompleteFunctionCall
{
public:
    CompleteFunctionCall(bool hasArguments = true) : hasArguments(hasArguments) {}
    bool hasArguments;
};

124 125 126
class CompletionAdder : public PropertyProcessor
{
protected:
127
    QList<AssistProposalItem *> *completions;
128 129

public:
130
    CompletionAdder(QList<AssistProposalItem *> *completions,
131 132 133 134 135 136
                    const QIcon &icon, int order)
        : completions(completions)
        , icon(icon)
        , order(order)
    {}

137
    void operator()(const Value *base, const QString &name, const Value *value) Q_DECL_OVERRIDE
138 139
    {
        Q_UNUSED(base)
140 141 142 143
        QVariant data;
        if (const FunctionValue *func = value->asFunctionValue()) {
            // constructors usually also have other interesting members,
            // don't consider them pure functions and complete the '()'
144
            if (!func->lookupMember(QLatin1String("prototype"), 0, 0, false))
145 146 147
                data = QVariant::fromValue(CompleteFunctionCall(func->namedArgumentCount() || func->isVariadic()));
        }
        addCompletion(completions, name, icon, order, data);
148 149 150 151 152 153 154 155 156
    }

    QIcon icon;
    int order;
};

class LhsCompletionAdder : public CompletionAdder
{
public:
157
    LhsCompletionAdder(QList<AssistProposalItem *> *completions,
158 159 160 161 162 163 164
                       const QIcon &icon,
                       int order,
                       bool afterOn)
        : CompletionAdder(completions, icon, order)
        , afterOn(afterOn)
    {}

165
    void operator ()(const Value *base, const QString &name, const Value *) Q_DECL_OVERRIDE
166
    {
Christian Kamm's avatar
Christian Kamm committed
167
        const CppComponentValue *qmlBase = value_cast<CppComponentValue>(base);
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188

        QString itemText = name;
        QString postfix;
        if (!itemText.isEmpty() && itemText.at(0).isLower())
            postfix = QLatin1String(": ");
        if (afterOn)
            postfix = QLatin1String(" {");

        // readonly pointer properties (anchors, ...) always get a .
        if (qmlBase && !qmlBase->isWritable(name) && qmlBase->isPointer(name))
            postfix = QLatin1Char('.');

        itemText.append(postfix);

        addCompletion(completions, itemText, icon, order);
    }

    bool afterOn;
};

class ProcessProperties: private MemberProcessor
Leandro Melo's avatar
Leandro Melo committed
189
{
190
    QSet<const ObjectValue *> _processed;
Leandro Melo's avatar
Leandro Melo committed
191 192
    bool _globalCompletion;
    bool _enumerateGeneratedSlots;
193
    bool _enumerateSlots;
194 195
    const ScopeChain *_scopeChain;
    const ObjectValue *_currentObject;
196
    PropertyProcessor *_propertyProcessor;
Leandro Melo's avatar
Leandro Melo committed
197 198

public:
199
    ProcessProperties(const ScopeChain *scopeChain)
Leandro Melo's avatar
Leandro Melo committed
200 201
        : _globalCompletion(false),
          _enumerateGeneratedSlots(false),
202
          _enumerateSlots(true),
203
          _scopeChain(scopeChain),
204 205
          _currentObject(0),
          _propertyProcessor(0)
Leandro Melo's avatar
Leandro Melo committed
206 207 208 209 210 211 212 213 214 215 216 217 218
    {
    }

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

    void setEnumerateGeneratedSlots(bool enumerate)
    {
        _enumerateGeneratedSlots = enumerate;
    }

219 220 221 222 223
    void setEnumerateSlots(bool enumerate)
    {
        _enumerateSlots = enumerate;
    }

224
    void operator ()(const Value *value, PropertyProcessor *processor)
Leandro Melo's avatar
Leandro Melo committed
225 226
    {
        _processed.clear();
227
        _propertyProcessor = processor;
Leandro Melo's avatar
Leandro Melo committed
228

229
        processProperties(value);
Leandro Melo's avatar
Leandro Melo committed
230 231
    }

232
    void operator ()(PropertyProcessor *processor)
Leandro Melo's avatar
Leandro Melo committed
233 234
    {
        _processed.clear();
235
        _propertyProcessor = processor;
Leandro Melo's avatar
Leandro Melo committed
236

237
        foreach (const ObjectValue *scope, _scopeChain->all())
238
            processProperties(scope);
Leandro Melo's avatar
Leandro Melo committed
239 240 241
    }

private:
242
    void process(const QString &name, const Value *value)
Leandro Melo's avatar
Leandro Melo committed
243
    {
244
        (*_propertyProcessor)(_currentObject, name, value);
Leandro Melo's avatar
Leandro Melo committed
245 246
    }

247
    bool processProperty(const QString &name, const Value *value, const PropertyInfo &) Q_DECL_OVERRIDE
Leandro Melo's avatar
Leandro Melo committed
248
    {
249
        process(name, value);
Leandro Melo's avatar
Leandro Melo committed
250 251 252
        return true;
    }

253
    bool processEnumerator(const QString &name, const Value *value) Q_DECL_OVERRIDE
Leandro Melo's avatar
Leandro Melo committed
254 255
    {
        if (! _globalCompletion)
256
            process(name, value);
Leandro Melo's avatar
Leandro Melo committed
257 258 259
        return true;
    }

260
    bool processSignal(const QString &name, const Value *value) Q_DECL_OVERRIDE
Leandro Melo's avatar
Leandro Melo committed
261
    {
262
        if (_globalCompletion)
263
            process(name, value);
Leandro Melo's avatar
Leandro Melo committed
264 265 266
        return true;
    }

267
    bool processSlot(const QString &name, const Value *value) Q_DECL_OVERRIDE
Leandro Melo's avatar
Leandro Melo committed
268
    {
269
        if (_enumerateSlots)
270
            process(name, value);
Leandro Melo's avatar
Leandro Melo committed
271 272 273
        return true;
    }

274
    bool processGeneratedSlot(const QString &name, const Value *value) Q_DECL_OVERRIDE
Leandro Melo's avatar
Leandro Melo committed
275 276 277
    {
        if (_enumerateGeneratedSlots || (_currentObject && _currentObject->className().endsWith(QLatin1String("Keys")))) {
            // ### FIXME: add support for attached properties.
278
            process(name, value);
Leandro Melo's avatar
Leandro Melo committed
279 280 281 282
        }
        return true;
    }

283
    void processProperties(const Value *value)
Leandro Melo's avatar
Leandro Melo committed
284 285 286
    {
        if (! value)
            return;
287
        if (const ObjectValue *object = value->asObjectValue())
288
            processProperties(object);
Leandro Melo's avatar
Leandro Melo committed
289 290
    }

291
    void processProperties(const ObjectValue *object)
Leandro Melo's avatar
Leandro Melo committed
292 293 294 295 296
    {
        if (! object || _processed.contains(object))
            return;

        _processed.insert(object);
297
        processProperties(object->prototype(_scopeChain->context()));
Leandro Melo's avatar
Leandro Melo committed
298

299
        _currentObject = object;
Leandro Melo's avatar
Leandro Melo committed
300
        object->processMembers(this);
301
        _currentObject = 0;
Leandro Melo's avatar
Leandro Melo committed
302 303 304
    }
};

305
const Value *getPropertyValue(const ObjectValue *object,
Leandro Melo's avatar
Leandro Melo committed
306
                                           const QStringList &propertyNames,
307
                                           const ContextPtr &context)
Leandro Melo's avatar
Leandro Melo committed
308 309 310 311
{
    if (propertyNames.isEmpty() || !object)
        return 0;

312
    const Value *value = object;
Leandro Melo's avatar
Leandro Melo committed
313
    foreach (const QString &name, propertyNames) {
314
        if (const ObjectValue *objectValue = value->asObjectValue()) {
315
            value = objectValue->lookupMember(name, context);
Leandro Melo's avatar
Leandro Melo committed
316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
            if (!value)
                return 0;
        } else {
            return 0;
        }
    }
    return value;
}

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

} // Anonymous

337 338 339 340 341
} // namesapce QmlJSEditor

Q_DECLARE_METATYPE(QmlJSEditor::CompleteFunctionCall)

namespace QmlJSEditor {
342

Leandro Melo's avatar
Leandro Melo committed
343 344 345 346 347 348 349 350 351 352 353 354
// -----------------------
// QmlJSAssistProposalItem
// -----------------------
bool QmlJSAssistProposalItem::prematurelyApplies(const QChar &c) const
{
    if (data().canConvert<QString>()) // snippet
        return false;

    return (text().endsWith(QLatin1String(": ")) && c == QLatin1Char(':'))
            || (text().endsWith(QLatin1Char('.')) && c == QLatin1Char('.'));
}

355
void QmlJSAssistProposalItem::applyContextualContent(TextEditorWidget *editorWidget,
Leandro Melo's avatar
Leandro Melo committed
356 357
                                                      int basePosition) const
{
358 359 360
    const int currentPosition = editorWidget->position();
    editorWidget->setCursorPosition(basePosition);
    editorWidget->remove(currentPosition - basePosition);
Leandro Melo's avatar
Leandro Melo committed
361

362 363 364
    QString content = text();
    int cursorOffset = 0;

365 366
    const bool autoInsertBrackets =
        TextEditorSettings::completionSettings().m_autoInsertBrackets;
367 368 369 370 371 372 373 374 375

    if (autoInsertBrackets && data().canConvert<CompleteFunctionCall>()) {
        CompleteFunctionCall function = data().value<CompleteFunctionCall>();
        content += QLatin1String("()");
        if (function.hasArguments)
            cursorOffset = -1;
    }

    QString replaceable = content;
Leandro Melo's avatar
Leandro Melo committed
376 377 378
    int replacedLength = 0;
    for (int i = 0; i < replaceable.length(); ++i) {
        const QChar a = replaceable.at(i);
379
        const QChar b = editorWidget->characterAt(editorWidget->position() + i);
Leandro Melo's avatar
Leandro Melo committed
380 381 382 383 384
        if (a == b)
            ++replacedLength;
        else
            break;
    }
385 386
    const int length = editorWidget->position() - basePosition + replacedLength;
    editorWidget->replace(length, content);
387
    if (cursorOffset)
388
        editorWidget->setCursorPosition(editorWidget->position() + cursorOffset);
Leandro Melo's avatar
Leandro Melo committed
389 390 391 392 393
}

// -------------------------
// FunctionHintProposalModel
// -------------------------
394
class FunctionHintProposalModel : public IFunctionHintProposalModel
Leandro Melo's avatar
Leandro Melo committed
395 396
{
public:
397 398
    FunctionHintProposalModel(const QString &functionName, const QStringList &namedArguments,
                              int optionalNamedArguments, bool isVariadic)
Leandro Melo's avatar
Leandro Melo committed
399
        : m_functionName(functionName)
400 401 402
        , m_namedArguments(namedArguments)
        , m_optionalNamedArguments(optionalNamedArguments)
        , m_isVariadic(isVariadic)
Leandro Melo's avatar
Leandro Melo committed
403 404
    {}

405 406 407 408
    void reset() Q_DECL_OVERRIDE {}
    int size() const Q_DECL_OVERRIDE { return 1; }
    QString text(int index) const Q_DECL_OVERRIDE;
    int activeArgument(const QString &prefix) const Q_DECL_OVERRIDE;
Leandro Melo's avatar
Leandro Melo committed
409 410 411

private:
    QString m_functionName;
412 413 414
    QStringList m_namedArguments;
    int m_optionalNamedArguments;
    bool m_isVariadic;
Leandro Melo's avatar
Leandro Melo committed
415 416 417 418 419 420 421 422 423 424
};

QString FunctionHintProposalModel::text(int index) const
{
    Q_UNUSED(index)

    QString prettyMethod;
    prettyMethod += QString::fromLatin1("function ");
    prettyMethod += m_functionName;
    prettyMethod += QLatin1Char('(');
425 426 427
    for (int i = 0; i < m_namedArguments.size(); ++i) {
        if (i == m_namedArguments.size() - m_optionalNamedArguments)
            prettyMethod += QLatin1Char('[');
Leandro Melo's avatar
Leandro Melo committed
428 429 430
        if (i != 0)
            prettyMethod += QLatin1String(", ");

431
        QString arg = m_namedArguments.at(i);
Leandro Melo's avatar
Leandro Melo committed
432 433 434 435 436 437 438
        if (arg.isEmpty()) {
            arg = QLatin1String("arg");
            arg += QString::number(i + 1);
        }

        prettyMethod += arg;
    }
439 440 441 442 443 444 445
    if (m_isVariadic) {
        if (m_namedArguments.size())
            prettyMethod += QLatin1String(", ");
        prettyMethod += QLatin1String("...");
    }
    if (m_optionalNamedArguments)
        prettyMethod += QLatin1Char(']');
Leandro Melo's avatar
Leandro Melo committed
446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474
    prettyMethod += QLatin1Char(')');
    return prettyMethod;
}

int FunctionHintProposalModel::activeArgument(const QString &prefix) const
{
    int argnr = 0;
    int parcount = 0;
    Scanner tokenize;
    const QList<Token> tokens = tokenize(prefix);
    for (int i = 0; i < tokens.count(); ++i) {
        const Token &tk = tokens.at(i);
        if (tk.is(Token::LeftParenthesis))
            ++parcount;
        else if (tk.is(Token::RightParenthesis))
            --parcount;
        else if (! parcount && tk.is(Token::Colon))
            ++argnr;
    }

    if (parcount < 0)
        return -1;

    return argnr;
}

// -----------------------------
// QmlJSCompletionAssistProvider
// -----------------------------
475
bool QmlJSCompletionAssistProvider::supportsEditor(Core::Id editorId) const
Leandro Melo's avatar
Leandro Melo committed
476
{
477
    return editorId == Constants::C_QMLJSEDITOR_ID;
Leandro Melo's avatar
Leandro Melo committed
478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504
}

int QmlJSCompletionAssistProvider::activationCharSequenceLength() const
{
    return 1;
}

bool QmlJSCompletionAssistProvider::isActivationCharSequence(const QString &sequence) const
{
    return isActivationChar(sequence.at(0));
}

bool QmlJSCompletionAssistProvider::isContinuationChar(const QChar &c) const
{
    return isIdentifierChar(c, false);
}

IAssistProcessor *QmlJSCompletionAssistProvider::createProcessor() const
{
    return new QmlJSCompletionAssistProcessor;
}

// ------------------------------
// QmlJSCompletionAssistProcessor
// ------------------------------
QmlJSCompletionAssistProcessor::QmlJSCompletionAssistProcessor()
    : m_startPosition(0)
505
    , m_snippetCollector(QLatin1String(Constants::QML_SNIPPETS_GROUP_ID), iconForColor(Qt::red), SnippetOrder)
Leandro Melo's avatar
Leandro Melo committed
506 507 508 509 510 511 512
{}

QmlJSCompletionAssistProcessor::~QmlJSCompletionAssistProcessor()
{}

IAssistProposal *QmlJSCompletionAssistProcessor::createContentProposal() const
{
513
    GenericProposalModel *model = new QmlJSAssistProposalModel(m_completions);
hjk's avatar
hjk committed
514
    return new GenericProposal(m_startPosition, model);
Leandro Melo's avatar
Leandro Melo committed
515 516
}

517 518 519
IAssistProposal *QmlJSCompletionAssistProcessor::createHintProposal(
        const QString &functionName, const QStringList &namedArguments,
        int optionalNamedArguments, bool isVariadic) const
Leandro Melo's avatar
Leandro Melo committed
520
{
521 522
    IFunctionHintProposalModel *model = new FunctionHintProposalModel(
                functionName, namedArguments, optionalNamedArguments, isVariadic);
Leandro Melo's avatar
Leandro Melo committed
523 524 525 526
    IAssistProposal *proposal = new FunctionHintProposal(m_startPosition, model);
    return proposal;
}

527
IAssistProposal *QmlJSCompletionAssistProcessor::perform(const AssistInterface *assistInterface)
Leandro Melo's avatar
Leandro Melo committed
528 529 530 531 532 533
{
    m_interface.reset(static_cast<const QmlJSCompletionAssistInterface *>(assistInterface));

    if (assistInterface->reason() == IdleEditor && !acceptsIdleEditor())
        return 0;

534
    const QString &fileName = m_interface->fileName();
Leandro Melo's avatar
Leandro Melo committed
535 536

    m_startPosition = assistInterface->position();
537
    while (isIdentifierChar(m_interface->textDocument()->characterAt(m_startPosition - 1), false, false))
Leandro Melo's avatar
Leandro Melo committed
538
        --m_startPosition;
539
    const bool onIdentifier = m_startPosition != assistInterface->position();
Leandro Melo's avatar
Leandro Melo committed
540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555

    m_completions.clear();

    const QmlJSCompletionAssistInterface *qmlInterface =
            static_cast<const QmlJSCompletionAssistInterface *>(assistInterface);
    const SemanticInfo &semanticInfo = qmlInterface->semanticInfo();
    if (!semanticInfo.isValid())
        return 0;

    const Document::Ptr document = semanticInfo.document;
    const QFileInfo currentFileInfo(fileName);

    bool isQmlFile = false;
    if (currentFileInfo.suffix() == QLatin1String("qml"))
        isQmlFile = true;

556
    const QList<AST::Node *> path = semanticInfo.rangePath(m_interface->position());
557 558
    const ContextPtr &context = semanticInfo.context;
    const ScopeChain &scopeChain = semanticInfo.scopeChain(path);
Leandro Melo's avatar
Leandro Melo committed
559

560 561 562 563 564
    // The completionOperator is the character under the cursor or directly before the
    // identifier under cursor. Use in conjunction with onIdentifier. Examples:
    // a + b<complete> -> ' '
    // a +<complete> -> '+'
    // a +b<complete> -> '+'
Leandro Melo's avatar
Leandro Melo committed
565 566
    QChar completionOperator;
    if (m_startPosition > 0)
567
        completionOperator = m_interface->textDocument()->characterAt(m_startPosition - 1);
Leandro Melo's avatar
Leandro Melo committed
568

569
    QTextCursor startPositionCursor(qmlInterface->textDocument());
Leandro Melo's avatar
Leandro Melo committed
570 571 572
    startPositionCursor.setPosition(m_startPosition);
    CompletionContextFinder contextFinder(startPositionCursor);

573
    const ObjectValue *qmlScopeType = 0;
Leandro Melo's avatar
Leandro Melo committed
574
    if (contextFinder.isInQmlContext()) {
575
        // find the enclosing qml object
Leandro Melo's avatar
Leandro Melo committed
576
        // ### this should use semanticInfo.declaringMember instead, but that may also return functions
577 578
        int i;
        for (i = path.size() - 1; i >= 0; --i) {
Leandro Melo's avatar
Leandro Melo committed
579 580 581 582 583 584 585
            AST::Node *node = path[i];
            if (AST::cast<AST::UiObjectDefinition *>(node) || AST::cast<AST::UiObjectBinding *>(node)) {
                qmlScopeType = document->bind()->findQmlObject(node);
                if (qmlScopeType)
                    break;
            }
        }
586 587 588 589 590
        // grouped property bindings change the scope type
        for (i++; i < path.size(); ++i) {
            AST::UiObjectDefinition *objDef = AST::cast<AST::UiObjectDefinition *>(path[i]);
            if (!objDef || !document->bind()->isGroupedPropertyBinding(objDef))
                break;
591
            const ObjectValue *newScopeType = qmlScopeType;
592
            for (AST::UiQualifiedId *it = objDef->qualifiedTypeNameId; it; it = it->next) {
593
                if (!newScopeType || it->name.isEmpty()) {
594 595 596
                    newScopeType = 0;
                    break;
                }
597
                const Value *v = newScopeType->lookupMember(it->name.toString(), context);
598
                v = context->lookupReference(v);
Christian Kamm's avatar
Christian Kamm committed
599
                newScopeType = value_cast<ObjectValue>(v);
600 601 602 603 604
            }
            if (!newScopeType)
                break;
            qmlScopeType = newScopeType;
        }
Leandro Melo's avatar
Leandro Melo committed
605 606 607 608 609 610 611 612
        // fallback to getting the base type object
        if (!qmlScopeType)
            qmlScopeType = context->lookupType(document.data(), contextFinder.qmlObjectTypeName());
    }

    if (contextFinder.isInStringLiteral()) {
        // get the text of the literal up to the cursor position
        //QTextCursor tc = textWidget->textCursor();
613
        QTextCursor tc(qmlInterface->textDocument());
Leandro Melo's avatar
Leandro Melo committed
614 615 616 617
        tc.setPosition(qmlInterface->position());
        QmlExpressionUnderCursor expressionUnderCursor;
        expressionUnderCursor(tc);
        QString literalText = expressionUnderCursor.text();
618 619 620 621 622 623 624 625 626

        // expression under cursor only looks at one line, so multi-line strings
        // are handled incorrectly and are recognizable by don't starting with ' or "
        if (!literalText.isEmpty()
                && literalText.at(0) != QLatin1Char('"')
                && literalText.at(0) != QLatin1Char('\'')) {
            return 0;
        }

Leandro Melo's avatar
Leandro Melo committed
627 628 629 630 631 632 633 634 635 636
        literalText = literalText.mid(1);

        if (contextFinder.isInImport()) {
            QStringList patterns;
            patterns << QLatin1String("*.qml") << QLatin1String("*.js");
            if (completeFileName(document->path(), literalText, patterns))
                return createContentProposal();
            return 0;
        }

637
        const Value *value =
Leandro Melo's avatar
Leandro Melo committed
638 639 640 641 642 643 644 645 646 647 648
                getPropertyValue(qmlScopeType, contextFinder.bindingPropertyName(), context);
        if (!value) {
            // do nothing
        } else if (value->asUrlValue()) {
            if (completeUrl(document->path(), literalText))
                return createContentProposal();
        }

        // ### enum completion?

        return 0;
649
    }
650 651

    // currently path-in-stringliteral is the only completion available in imports
652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671
    if (contextFinder.isInImport()) {
        QmlJS::ModelManagerInterface::ProjectInfo pInfo = QmlJS::ModelManagerInterface::instance()
                ->projectInfo(ProjectExplorer::ProjectExplorerPlugin::currentProject());
        QmlBundle platform = pInfo.extendedBundle.bundleForLanguage(document->language());
        if (!platform.supportedImports().isEmpty()) {
            QTextCursor tc(qmlInterface->textDocument());
            tc.setPosition(qmlInterface->position());
            QmlExpressionUnderCursor expressionUnderCursor;
            expressionUnderCursor(tc);
            QString libVersion = contextFinder.libVersionImport();
            if (!libVersion.isNull()) {
                QStringList completions=platform.supportedImports().complete(libVersion, QString(), QmlJS::PersistentTrie::LookupFlags(QmlJS::PersistentTrie::CaseInsensitive|QmlJS::PersistentTrie::SkipChars|QmlJS::PersistentTrie::SkipSpaces));
                completions = QmlJS::PersistentTrie::matchStrengthSort(libVersion, completions);

                int toSkip = qMax(libVersion.lastIndexOf(QLatin1Char(' '))
                                  , libVersion.lastIndexOf(QLatin1Char('.')));
                if (++toSkip > 0) {
                    QStringList nCompletions;
                    QString prefix(libVersion.left(toSkip));
                    nCompletions.reserve(completions.size());
672
                    foreach (const QString &completion, completions)
673 674 675 676 677 678 679 680
                        if (completion.startsWith(prefix))
                            nCompletions.append(completion.right(completion.size()-toSkip));
                    completions = nCompletions;
                }
                addCompletions(&m_completions, completions, m_interface->fileNameIcon(), KeywordOrder);
                return createContentProposal();
            }
        }
681
        return 0;
682
    }
683

684
    // member "a.bc<complete>" or function "foo(<complete>" completion
685 686
    if (completionOperator == QLatin1Char('.')
            || (completionOperator == QLatin1Char('(') && !onIdentifier)) {
687 688
        // Look at the expression under cursor.
        //QTextCursor tc = textWidget->textCursor();
689
        QTextCursor tc(qmlInterface->textDocument());
690 691 692 693 694 695 696 697 698 699
        tc.setPosition(m_startPosition - 1);

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

        if (expression != 0 && ! isLiteral(expression)) {
            // Evaluate the expression under cursor.
            ValueOwner *interp = context->valueOwner();
            const Value *value =
                    interp->convertToObject(scopeChain.evaluate(expression));
700
            //qCDebug(qmljsLog) << "type:" << interp->typeId(value);
701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722

            if (value && completionOperator == QLatin1Char('.')) { // member completion
                ProcessProperties processProperties(&scopeChain);
                if (contextFinder.isInLhsOfBinding() && qmlScopeType) {
                    LhsCompletionAdder completionAdder(&m_completions, m_interface->symbolIcon(),
                                                       PropertyOrder, contextFinder.isAfterOnInLhsOfBinding());
                    processProperties.setEnumerateGeneratedSlots(true);
                    processProperties(value, &completionAdder);
                } else {
                    CompletionAdder completionAdder(&m_completions, m_interface->symbolIcon(), SymbolOrder);
                    processProperties(value, &completionAdder);
                }
            } else if (value
                       && completionOperator == QLatin1Char('(')
                       && m_startPosition == m_interface->position()) {
                // function completion
                if (const FunctionValue *f = value->asFunctionValue()) {
                    QString functionName = expressionUnderCursor.text();
                    int indexOfDot = functionName.lastIndexOf(QLatin1Char('.'));
                    if (indexOfDot != -1)
                        functionName = functionName.mid(indexOfDot + 1);

723 724 725
                    QStringList namedArguments;
                    for (int i = 0; i < f->namedArgumentCount(); ++i)
                        namedArguments.append(f->argumentName(i));
726

727 728
                    return createHintProposal(functionName.trimmed(), namedArguments,
                                              f->optionalNamedArgumentCount(), f->isVariadic());
729 730 731 732 733 734 735 736
                }
            }
        }

        if (! m_completions.isEmpty())
            return createContentProposal();
        return 0;
    }
737

738
    // global completion
739
    if (onIdentifier || assistInterface->reason() == ExplicitlyInvoked) {
Leandro Melo's avatar
Leandro Melo committed
740 741 742 743 744 745 746 747 748 749 750

        bool doGlobalCompletion = true;
        bool doQmlKeywordCompletion = true;
        bool doJsKeywordCompletion = true;
        bool doQmlTypeCompletion = false;

        if (contextFinder.isInLhsOfBinding() && qmlScopeType) {
            doGlobalCompletion = false;
            doJsKeywordCompletion = false;
            doQmlTypeCompletion = true;

751 752 753 754
            ProcessProperties processProperties(&scopeChain);
            processProperties.setGlobalCompletion(true);
            processProperties.setEnumerateGeneratedSlots(true);
            processProperties.setEnumerateSlots(false);
Leandro Melo's avatar
Leandro Melo committed
755 756

            // id: is special
757
            AssistProposalItem *idProposalItem = new QmlJSAssistProposalItem;
Leandro Melo's avatar
Leandro Melo committed
758 759 760 761 762
            idProposalItem->setText(QLatin1String("id: "));
            idProposalItem->setIcon(m_interface->symbolIcon());
            idProposalItem->setOrder(PropertyOrder);
            m_completions.append(idProposalItem);

763 764 765 766 767
            {
                LhsCompletionAdder completionAdder(&m_completions, m_interface->symbolIcon(),
                                                   PropertyOrder, contextFinder.isAfterOnInLhsOfBinding());
                processProperties(qmlScopeType, &completionAdder);
            }
Leandro Melo's avatar
Leandro Melo committed
768 769

            if (ScopeBuilder::isPropertyChangesObject(context, qmlScopeType)
770
                    && scopeChain.qmlScopeObjects().size() == 2) {
771 772
                CompletionAdder completionAdder(&m_completions, m_interface->symbolIcon(), SymbolOrder);
                processProperties(scopeChain.qmlScopeObjects().first(), &completionAdder);
Leandro Melo's avatar
Leandro Melo committed
773 774 775 776 777 778 779
            }
        }

        if (contextFinder.isInRhsOfBinding() && qmlScopeType) {
            doQmlKeywordCompletion = false;

            // complete enum values for enum properties
780
            const Value *value =
Leandro Melo's avatar
Leandro Melo committed
781
                    getPropertyValue(qmlScopeType, contextFinder.bindingPropertyName(), context);
782
            if (const QmlEnumValue *enumValue =
Christian Kamm's avatar
Christian Kamm committed
783
                    value_cast<QmlEnumValue>(value)) {
784 785 786 787
                const QString &name = context->imports(document.data())->nameForImportedObject(enumValue->owner(), context.data());
                foreach (const QString &key, enumValue->keys()) {
                    QString completion;
                    if (name.isEmpty())
788
                        completion = QString::fromLatin1("\"%1\"").arg(key);
789
                    else
790
                        completion = QString::fromLatin1("%1.%2").arg(name, key);
791
                    addCompletion(&m_completions, key, m_interface->symbolIcon(),
792 793
                                  EnumValueOrder, completion);
                }
Leandro Melo's avatar
Leandro Melo committed
794 795 796 797 798 799 800
            }
        }

        if (!contextFinder.isInImport() && !contextFinder.isInQmlContext())
            doQmlTypeCompletion = true;

        if (doQmlTypeCompletion) {
801
            if (const ObjectValue *qmlTypes = scopeChain.qmlTypes()) {
802 803 804
                ProcessProperties processProperties(&scopeChain);
                CompletionAdder completionAdder(&m_completions, m_interface->symbolIcon(), TypeOrder);
                processProperties(qmlTypes, &completionAdder);
Leandro Melo's avatar
Leandro Melo committed
805 806 807 808 809
            }
        }

        if (doGlobalCompletion) {
            // It's a global completion.
810 811 812 813
            ProcessProperties processProperties(&scopeChain);
            processProperties.setGlobalCompletion(true);
            CompletionAdder completionAdder(&m_completions, m_interface->symbolIcon(), SymbolOrder);
            processProperties(&completionAdder);
Leandro Melo's avatar
Leandro Melo committed
814 815 816 817
        }

        if (doJsKeywordCompletion) {
            // add js keywords
818
            addCompletions(&m_completions, Scanner::keywords(), m_interface->keywordIcon(), KeywordOrder);
Leandro Melo's avatar
Leandro Melo committed
819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834
        }

        // add qml extra words
        if (doQmlKeywordCompletion && isQmlFile) {
            static QStringList qmlWords;
            static QStringList qmlWordsAlsoInJs;

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

835
            addCompletions(&m_completions, qmlWords, m_interface->keywordIcon(), KeywordOrder);
Leandro Melo's avatar
Leandro Melo committed
836
            if (!doJsKeywordCompletion)
837
                addCompletions(&m_completions, qmlWordsAlsoInJs, m_interface->keywordIcon(), KeywordOrder);
Leandro Melo's avatar
Leandro Melo committed
838 839
        }

840
        m_completions.append(m_snippetCollector.collect());
Leandro Melo's avatar
Leandro Melo committed
841 842 843 844 845 846 847 848 849 850 851 852 853 854

        if (! m_completions.isEmpty())
            return createContentProposal();
        return 0;
    }

    return 0;
}

bool QmlJSCompletionAssistProcessor::acceptsIdleEditor() const
{
    const int cursorPos = m_interface->position();

    bool maybeAccept = false;
855
    const QChar &charBeforeCursor = m_interface->textDocument()->characterAt(cursorPos - 1);
Leandro Melo's avatar
Leandro Melo committed
856 857 858
    if (isActivationChar(charBeforeCursor)) {
        maybeAccept = true;
    } else {
859
        const QChar &charUnderCursor = m_interface->textDocument()->characterAt(cursorPos);
Leandro Melo's avatar
Leandro Melo committed
860 861 862 863 864 865 866 867
        if (isIdentifierChar(charBeforeCursor)
                && ((charUnderCursor.isSpace()
                    || charUnderCursor.isNull()
                    || isDelimiterChar(charUnderCursor))
                || isIdentifierChar(charUnderCursor))) {

            int startPos = cursorPos - 1;
            for (; startPos != -1; --startPos) {
868
                if (!isIdentifierChar(m_interface->textDocument()->characterAt(startPos)))
Leandro Melo's avatar
Leandro Melo committed
869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884
                    break;
            }
            ++startPos;

            const QString &word = m_interface->textAt(startPos, cursorPos - startPos);
            if (word.length() > 2 && isIdentifierChar(word.at(0), true)) {
                for (int i = 1; i < word.length(); ++i) {
                    if (!isIdentifierChar(word.at(i)))
                        return false;
                }
                maybeAccept = true;
            }
        }
    }

    if (maybeAccept) {
885
        QTextCursor tc(m_interface->textDocument());
Leandro Melo's avatar
Leandro Melo committed
886 887 888 889 890 891 892 893 894 895 896 897
        tc.setPosition(m_interface->position());
        const QTextBlock &block = tc.block();
        const QString &blockText = block.text();
        const int blockState = qMax(0, block.previous().userState()) & 0xff;

        Scanner scanner;
        const QList<Token> tokens = scanner(blockText, blockState);
        const int column = block.position() - m_interface->position();
        foreach (const Token &tk, tokens) {
            if (column >= tk.begin() && column <= tk.end()) {
                if (charBeforeCursor == QLatin1Char('/') && tk.is(Token::String))
                    return true; // path completion inside string literals
898
                if (tk.is(Token::Comment) || tk.is(Token::String) || tk.is(Token::RegExp))
Leandro Melo's avatar
Leandro Melo committed
899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932
                    return false;
                break;
            }
        }
        if (charBeforeCursor != QLatin1Char('/'))
            return true;
    }

    return false;
}

bool QmlJSCompletionAssistProcessor::completeFileName(const QString &relativeBasePath,
                                                      const QString &fileName,
                                                      const QStringList &patterns)
{
    const QFileInfo fileInfo(fileName);
    QString directoryPrefix;
    if (fileInfo.isRelative()) {
        directoryPrefix = relativeBasePath;
        directoryPrefix += QDir::separator();
        directoryPrefix += fileInfo.path();
    } else {
        directoryPrefix = fileInfo.path();
    }
    if (!QFileInfo(directoryPrefix).exists())
        return false;

    QDirIterator dirIterator(directoryPrefix,
                             patterns,
                             QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot);
    while (dirIterator.hasNext()) {
        dirIterator.next();
        const QString fileName = dirIterator.fileName();

933
        AssistProposalItem *item = new QmlJSAssistProposalItem;
Leandro Melo's avatar
Leandro Melo committed
934 935 936 937 938 939 940 941 942 943 944
        item->setText(fileName);
        item->setIcon(m_interface->fileNameIcon());
        m_completions.append(item);
    }

    return !m_completions.isEmpty();
}

bool QmlJSCompletionAssistProcessor::completeUrl(const QString &relativeBasePath, const QString &urlString)
{
    const QUrl url(urlString);
945
    QString fileName;
Christian Kamm's avatar
Christian Kamm committed
946
    if (url.scheme().compare(QLatin1String("file"), Qt::CaseInsensitive) == 0) {
947 948 949 950 951 952 953 954 955 956
        fileName = url.toLocalFile();
        // should not trigger completion on 'file://'
        if (fileName.isEmpty())
            return false;
    } else if (url.scheme().isEmpty()) {
        // don't trigger completion while typing a scheme
        if (urlString.endsWith(QLatin1String(":/")))
            return false;
        fileName = urlString;
    } else {
Leandro Melo's avatar
Leandro Melo committed
957
        return false;
958
    }
Leandro Melo's avatar
Leandro Melo committed
959 960 961 962 963 964 965

    return completeFileName(relativeBasePath, fileName);
}

// ------------------------------
// QmlJSCompletionAssistInterface
// ------------------------------
966
QmlJSCompletionAssistInterface::QmlJSCompletionAssistInterface(QTextDocument *textDocument,
Leandro Melo's avatar
Leandro Melo committed
967
                                                               int position,
968
                                                               const QString &fileName,
969
                                                               AssistReason reason,
Leandro Melo's avatar
Leandro Melo committed
970
                                                               const SemanticInfo &info)
971
    : AssistInterface(textDocument, position, fileName, reason)
Leandro Melo's avatar
Leandro Melo committed
972 973 974 975 976 977 978 979 980 981 982 983 984
    , m_semanticInfo(info)
    , m_darkBlueIcon(iconForColor(Qt::darkBlue))
    , m_darkYellowIcon(iconForColor(Qt::darkYellow))
    , m_darkCyanIcon(iconForColor(Qt::darkCyan))
{}

const SemanticInfo &QmlJSCompletionAssistInterface::semanticInfo() const
{
    return m_semanticInfo;
}

namespace {

985
class QmlJSLessThan
Leandro Melo's avatar
Leandro Melo committed
986
{
987 988 989
public:
    QmlJSLessThan(const QString &searchString) : m_searchString(searchString)
    { }
990
    bool operator() (const AssistProposalItem *a, const AssistProposalItem *b)
Leandro Melo's avatar
Leandro Melo committed
991 992 993
    {
        if (a->order() != b->order())
            return a->order() > b->order();
994
        else if (a->text().isEmpty() && ! b->text().isEmpty())
Leandro Melo's avatar
Leandro Melo committed
995 996 997 998 999 1000 1001 1002 1003
            return true;
        else if (b->text().isEmpty())
            return false;
        else if (a->data().isValid() != b->data().isValid())
            return a->data().isValid();
        else if (a->text().at(0).isUpper() && b->text().at(0).isLower())
            return false;
        else if (a->text().at(0).isLower() && b->text().at(0).isUpper())
            return true;
1004 1005 1006 1007
        int m1 = PersistentTrie::matchStrength(m_searchString, a->text());
        int m2 = PersistentTrie::matchStrength(m_searchString, b->text());
        if (m1 != m2)
            return m1 > m2;
Leandro Melo's avatar
Leandro Melo committed
1008 1009
        return a->text() < b->text();
    }
1010 1011
private:
    QString m_searchString;
Leandro Melo's avatar
Leandro Melo committed
1012 1013 1014 1015 1016 1017 1018
};

} // Anonymous

// -------------------------
// QmlJSAssistProposalModel
// -------------------------
1019 1020
void QmlJSAssistProposalModel::filter(const QString &prefix)
{
1021
    GenericProposalModel::filter(prefix);
1022 1023
    if (prefix.startsWith(QLatin1String("__")))
        return;
1024
    QList<AssistProposalItem *> newCurrentItems;
1025
    newCurrentItems.reserve(m_currentItems.size());
1026
    foreach (AssistProposalItem *item, m_currentItems)
1027 1028 1029 1030 1031
        if (!item->text().startsWith(QLatin1String("__")))
            newCurrentItems << item;