qmljscompletionassist.cpp 36.3 KB
Newer Older
hjk's avatar
hjk committed
1
/****************************************************************************
Leandro Melo's avatar
Leandro Melo committed
2
**
3
** Copyright (C) 2013 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 38 39 40

#include <texteditor/codeassist/iassistinterface.h>
#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 64 65
#include <QFile>
#include <QFileInfo>
#include <QDir>
#include <QDebug>
#include <QtAlgorithms>
#include <QDirIterator>
#include <QStringList>
#include <QIcon>
66
#include <QTextDocumentFragment>
Leandro Melo's avatar
Leandro Melo committed
67 68

using namespace QmlJS;
69
using namespace QmlJSTools;
Leandro Melo's avatar
Leandro Melo committed
70 71
using namespace TextEditor;

72 73 74 75
namespace QmlJSEditor {

using namespace Internal;

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

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

87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
static void addCompletion(QList<TextEditor::BasicProposalItem *> *completions,
                          const QString &text,
                          const QIcon &icon,
                          int order,
                          const QVariant &data = QVariant())
{
    if (text.isEmpty())
        return;

    BasicProposalItem *item = new QmlJSAssistProposalItem;
    item->setText(text);
    item->setIcon(icon);
    item->setOrder(order);
    item->setData(data);
    completions->append(item);
}

static void addCompletions(QList<TextEditor::BasicProposalItem *> *completions,
                           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;
};

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

126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
class CompletionAdder : public PropertyProcessor
{
protected:
    QList<TextEditor::BasicProposalItem *> *completions;

public:
    CompletionAdder(QList<TextEditor::BasicProposalItem *> *completions,
                    const QIcon &icon, int order)
        : completions(completions)
        , icon(icon)
        , order(order)
    {}

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

    QIcon icon;
    int order;
};

class LhsCompletionAdder : public CompletionAdder
{
public:
    LhsCompletionAdder(QList<TextEditor::BasicProposalItem *> *completions,
                       const QIcon &icon,
                       int order,
                       bool afterOn)
        : CompletionAdder(completions, icon, order)
        , afterOn(afterOn)
    {}

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

        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
191
{
192
    QSet<const ObjectValue *> _processed;
Leandro Melo's avatar
Leandro Melo committed
193 194
    bool _globalCompletion;
    bool _enumerateGeneratedSlots;
195
    bool _enumerateSlots;
196 197
    const ScopeChain *_scopeChain;
    const ObjectValue *_currentObject;
198
    PropertyProcessor *_propertyProcessor;
Leandro Melo's avatar
Leandro Melo committed
199 200

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

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

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

221 222 223 224 225
    void setEnumerateSlots(bool enumerate)
    {
        _enumerateSlots = enumerate;
    }

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

231
        processProperties(value);
Leandro Melo's avatar
Leandro Melo committed
232 233
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

314
    const Value *value = object;
Leandro Melo's avatar
Leandro Melo committed
315
    foreach (const QString &name, propertyNames) {
316
        if (const ObjectValue *objectValue = value->asObjectValue()) {
317
            value = objectValue->lookupMember(name, context);
Leandro Melo's avatar
Leandro Melo committed
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
            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

339 340 341 342 343
} // namesapce QmlJSEditor

Q_DECLARE_METATYPE(QmlJSEditor::CompleteFunctionCall)

namespace QmlJSEditor {
344

Leandro Melo's avatar
Leandro Melo committed
345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363
// -----------------------
// 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('.'));
}

void QmlJSAssistProposalItem::applyContextualContent(TextEditor::BaseTextEditor *editor,
                                                      int basePosition) const
{
    const int currentPosition = editor->position();
    editor->setCursorPosition(basePosition);
    editor->remove(currentPosition - basePosition);

364 365 366
    QString content = text();
    int cursorOffset = 0;

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

    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
378 379 380
    int replacedLength = 0;
    for (int i = 0; i < replaceable.length(); ++i) {
        const QChar a = replaceable.at(i);
381
        const QChar b = editor->textDocument()->characterAt(editor->position() + i);
Leandro Melo's avatar
Leandro Melo committed
382 383 384 385 386 387 388
        if (a == b)
            ++replacedLength;
        else
            break;
    }
    const int length = editor->position() - basePosition + replacedLength;
    editor->replace(length, content);
389 390
    if (cursorOffset)
        editor->setCursorPosition(editor->position() + cursorOffset);
Leandro Melo's avatar
Leandro Melo committed
391 392 393 394 395 396 397 398
}

// -------------------------
// FunctionHintProposalModel
// -------------------------
class FunctionHintProposalModel : public TextEditor::IFunctionHintProposalModel
{
public:
399 400
    FunctionHintProposalModel(const QString &functionName, const QStringList &namedArguments,
                              int optionalNamedArguments, bool isVariadic)
Leandro Melo's avatar
Leandro Melo committed
401
        : m_functionName(functionName)
402 403 404
        , m_namedArguments(namedArguments)
        , m_optionalNamedArguments(optionalNamedArguments)
        , m_isVariadic(isVariadic)
Leandro Melo's avatar
Leandro Melo committed
405 406 407 408 409 410 411 412 413
    {}

    virtual void reset() {}
    virtual int size() const { return 1; }
    virtual QString text(int index) const;
    virtual int activeArgument(const QString &prefix) const;

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

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

    QString prettyMethod;
    prettyMethod += QString::fromLatin1("function ");
    prettyMethod += m_functionName;
    prettyMethod += QLatin1Char('(');
427 428 429
    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
430 431 432
        if (i != 0)
            prettyMethod += QLatin1String(", ");

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

        prettyMethod += arg;
    }
441 442 443 444 445 446 447
    if (m_isVariadic) {
        if (m_namedArguments.size())
            prettyMethod += QLatin1String(", ");
        prettyMethod += QLatin1String("...");
    }
    if (m_optionalNamedArguments)
        prettyMethod += QLatin1Char(']');
Leandro Melo's avatar
Leandro Melo committed
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 475 476
    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
// -----------------------------
hjk's avatar
hjk committed
477
bool QmlJSCompletionAssistProvider::supportsEditor(const Core::Id &editorId) const
Leandro Melo's avatar
Leandro Melo committed
478
{
479
    return editorId == Constants::C_QMLJSEDITOR_ID;
Leandro Melo's avatar
Leandro Melo committed
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 505 506
}

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)
507
    , m_snippetCollector(QLatin1String(Constants::QML_SNIPPETS_GROUP_ID), iconForColor(Qt::red), SnippetOrder)
Leandro Melo's avatar
Leandro Melo committed
508 509 510 511 512
{}

QmlJSCompletionAssistProcessor::~QmlJSCompletionAssistProcessor()
{}

513 514 515 516 517
IAssistProposal *QmlJSCompletionAssistProcessor::immediateProposal(const IAssistInterface *)
{
    return 0;
}

Leandro Melo's avatar
Leandro Melo committed
518 519 520 521 522 523 524
IAssistProposal *QmlJSCompletionAssistProcessor::createContentProposal() const
{
    IGenericProposalModel *model = new QmlJSAssistProposalModel(m_completions);
    IAssistProposal *proposal = new GenericProposal(m_startPosition, model);
    return proposal;
}

525 526 527
IAssistProposal *QmlJSCompletionAssistProcessor::createHintProposal(
        const QString &functionName, const QStringList &namedArguments,
        int optionalNamedArguments, bool isVariadic) const
Leandro Melo's avatar
Leandro Melo committed
528
{
529 530
    IFunctionHintProposalModel *model = new FunctionHintProposalModel(
                functionName, namedArguments, optionalNamedArguments, isVariadic);
Leandro Melo's avatar
Leandro Melo committed
531 532 533 534 535 536 537 538 539 540 541
    IAssistProposal *proposal = new FunctionHintProposal(m_startPosition, model);
    return proposal;
}

IAssistProposal *QmlJSCompletionAssistProcessor::perform(const IAssistInterface *assistInterface)
{
    m_interface.reset(static_cast<const QmlJSCompletionAssistInterface *>(assistInterface));

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

542
    const QString &fileName = m_interface->fileName();
Leandro Melo's avatar
Leandro Melo committed
543 544

    m_startPosition = assistInterface->position();
545
    while (isIdentifierChar(m_interface->textDocument()->characterAt(m_startPosition - 1), false, false))
Leandro Melo's avatar
Leandro Melo committed
546
        --m_startPosition;
547
    const bool onIdentifier = m_startPosition != assistInterface->position();
Leandro Melo's avatar
Leandro Melo committed
548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563

    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;

564
    const QList<AST::Node *> path = semanticInfo.rangePath(m_interface->position());
565 566
    const ContextPtr &context = semanticInfo.context;
    const ScopeChain &scopeChain = semanticInfo.scopeChain(path);
Leandro Melo's avatar
Leandro Melo committed
567

568 569 570 571 572
    // 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
573 574
    QChar completionOperator;
    if (m_startPosition > 0)
575
        completionOperator = m_interface->textDocument()->characterAt(m_startPosition - 1);
Leandro Melo's avatar
Leandro Melo committed
576

577
    QTextCursor startPositionCursor(qmlInterface->textDocument());
Leandro Melo's avatar
Leandro Melo committed
578 579 580
    startPositionCursor.setPosition(m_startPosition);
    CompletionContextFinder contextFinder(startPositionCursor);

581
    const ObjectValue *qmlScopeType = 0;
Leandro Melo's avatar
Leandro Melo committed
582
    if (contextFinder.isInQmlContext()) {
583
        // find the enclosing qml object
Leandro Melo's avatar
Leandro Melo committed
584
        // ### this should use semanticInfo.declaringMember instead, but that may also return functions
585 586
        int i;
        for (i = path.size() - 1; i >= 0; --i) {
Leandro Melo's avatar
Leandro Melo committed
587 588 589 590 591 592 593
            AST::Node *node = path[i];
            if (AST::cast<AST::UiObjectDefinition *>(node) || AST::cast<AST::UiObjectBinding *>(node)) {
                qmlScopeType = document->bind()->findQmlObject(node);
                if (qmlScopeType)
                    break;
            }
        }
594 595 596 597 598
        // 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;
599
            const ObjectValue *newScopeType = qmlScopeType;
600
            for (AST::UiQualifiedId *it = objDef->qualifiedTypeNameId; it; it = it->next) {
601
                if (!newScopeType || it->name.isEmpty()) {
602 603 604
                    newScopeType = 0;
                    break;
                }
605
                const Value *v = newScopeType->lookupMember(it->name.toString(), context);
606
                v = context->lookupReference(v);
Christian Kamm's avatar
Christian Kamm committed
607
                newScopeType = value_cast<ObjectValue>(v);
608 609 610 611 612
            }
            if (!newScopeType)
                break;
            qmlScopeType = newScopeType;
        }
Leandro Melo's avatar
Leandro Melo committed
613 614 615 616 617 618 619 620
        // 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();
621
        QTextCursor tc(qmlInterface->textDocument());
Leandro Melo's avatar
Leandro Melo committed
622 623 624 625
        tc.setPosition(qmlInterface->position());
        QmlExpressionUnderCursor expressionUnderCursor;
        expressionUnderCursor(tc);
        QString literalText = expressionUnderCursor.text();
626 627 628 629 630 631 632 633 634

        // 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
635 636 637 638 639 640 641 642 643 644
        literalText = literalText.mid(1);

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

645
        const Value *value =
Leandro Melo's avatar
Leandro Melo committed
646 647 648 649 650 651 652 653 654 655 656
                getPropertyValue(qmlScopeType, contextFinder.bindingPropertyName(), context);
        if (!value) {
            // do nothing
        } else if (value->asUrlValue()) {
            if (completeUrl(document->path(), literalText))
                return createContentProposal();
        }

        // ### enum completion?

        return 0;
657
    }
658 659

    // currently path-in-stringliteral is the only completion available in imports
660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679
    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());
680
                    foreach (const QString &completion, completions)
681 682 683 684 685 686 687 688
                        if (completion.startsWith(prefix))
                            nCompletions.append(completion.right(completion.size()-toSkip));
                    completions = nCompletions;
                }
                addCompletions(&m_completions, completions, m_interface->fileNameIcon(), KeywordOrder);
                return createContentProposal();
            }
        }
689
        return 0;
690
    }
691

692
    // member "a.bc<complete>" or function "foo(<complete>" completion
693 694
    if (completionOperator == QLatin1Char('.')
            || (completionOperator == QLatin1Char('(') && !onIdentifier)) {
695 696
        // Look at the expression under cursor.
        //QTextCursor tc = textWidget->textCursor();
697
        QTextCursor tc(qmlInterface->textDocument());
698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730
        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));
            //qDebug() << "type:" << interp->typeId(value);

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

731 732 733
                    QStringList namedArguments;
                    for (int i = 0; i < f->namedArgumentCount(); ++i)
                        namedArguments.append(f->argumentName(i));
734

735 736
                    return createHintProposal(functionName.trimmed(), namedArguments,
                                              f->optionalNamedArgumentCount(), f->isVariadic());
737 738 739 740 741 742 743 744
                }
            }
        }

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

746
    // global completion
747
    if (onIdentifier || assistInterface->reason() == ExplicitlyInvoked) {
Leandro Melo's avatar
Leandro Melo committed
748 749 750 751 752 753 754 755 756 757 758

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

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

759 760 761 762
            ProcessProperties processProperties(&scopeChain);
            processProperties.setGlobalCompletion(true);
            processProperties.setEnumerateGeneratedSlots(true);
            processProperties.setEnumerateSlots(false);
Leandro Melo's avatar
Leandro Melo committed
763 764 765 766 767 768 769 770

            // id: is special
            BasicProposalItem *idProposalItem = new QmlJSAssistProposalItem;
            idProposalItem->setText(QLatin1String("id: "));
            idProposalItem->setIcon(m_interface->symbolIcon());
            idProposalItem->setOrder(PropertyOrder);
            m_completions.append(idProposalItem);

771 772 773 774 775
            {
                LhsCompletionAdder completionAdder(&m_completions, m_interface->symbolIcon(),
                                                   PropertyOrder, contextFinder.isAfterOnInLhsOfBinding());
                processProperties(qmlScopeType, &completionAdder);
            }
Leandro Melo's avatar
Leandro Melo committed
776 777

            if (ScopeBuilder::isPropertyChangesObject(context, qmlScopeType)
778
                    && scopeChain.qmlScopeObjects().size() == 2) {
779 780
                CompletionAdder completionAdder(&m_completions, m_interface->symbolIcon(), SymbolOrder);
                processProperties(scopeChain.qmlScopeObjects().first(), &completionAdder);
Leandro Melo's avatar
Leandro Melo committed
781 782 783 784 785 786 787
            }
        }

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

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

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

        if (doQmlTypeCompletion) {
809
            if (const ObjectValue *qmlTypes = scopeChain.qmlTypes()) {
810 811 812
                ProcessProperties processProperties(&scopeChain);
                CompletionAdder completionAdder(&m_completions, m_interface->symbolIcon(), TypeOrder);
                processProperties(qmlTypes, &completionAdder);
Leandro Melo's avatar
Leandro Melo committed
813 814 815 816 817
            }
        }

        if (doGlobalCompletion) {
            // It's a global completion.
818 819 820 821
            ProcessProperties processProperties(&scopeChain);
            processProperties.setGlobalCompletion(true);
            CompletionAdder completionAdder(&m_completions, m_interface->symbolIcon(), SymbolOrder);
            processProperties(&completionAdder);
Leandro Melo's avatar
Leandro Melo committed
822 823 824 825
        }

        if (doJsKeywordCompletion) {
            // add js keywords
826
            addCompletions(&m_completions, Scanner::keywords(), m_interface->keywordIcon(), KeywordOrder);
Leandro Melo's avatar
Leandro Melo committed
827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842
        }

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

843
            addCompletions(&m_completions, qmlWords, m_interface->keywordIcon(), KeywordOrder);
Leandro Melo's avatar
Leandro Melo committed
844
            if (!doJsKeywordCompletion)
845
                addCompletions(&m_completions, qmlWordsAlsoInJs, m_interface->keywordIcon(), KeywordOrder);
Leandro Melo's avatar
Leandro Melo committed
846 847
        }

848
        m_completions.append(m_snippetCollector.collect());
Leandro Melo's avatar
Leandro Melo committed
849 850 851 852 853 854 855 856 857 858 859 860 861 862

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

    return 0;
}

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

    bool maybeAccept = false;
863
    const QChar &charBeforeCursor = m_interface->textDocument()->characterAt(cursorPos - 1);
Leandro Melo's avatar
Leandro Melo committed
864 865 866
    if (isActivationChar(charBeforeCursor)) {
        maybeAccept = true;
    } else {
867
        const QChar &charUnderCursor = m_interface->textDocument()->characterAt(cursorPos);
Leandro Melo's avatar
Leandro Melo committed
868 869 870 871 872 873 874 875
        if (isIdentifierChar(charBeforeCursor)
                && ((charUnderCursor.isSpace()
                    || charUnderCursor.isNull()
                    || isDelimiterChar(charUnderCursor))
                || isIdentifierChar(charUnderCursor))) {

            int startPos = cursorPos - 1;
            for (; startPos != -1; --startPos) {
876
                if (!isIdentifierChar(m_interface->textDocument()->characterAt(startPos)))
Leandro Melo's avatar
Leandro Melo committed
877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892
                    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) {
893
        QTextCursor tc(m_interface->textDocument());
Leandro Melo's avatar
Leandro Melo committed
894 895 896 897 898 899 900 901 902 903 904 905
        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
906
                if (tk.is(Token::Comment) || tk.is(Token::String) || tk.is(Token::RegExp))
Leandro Melo's avatar
Leandro Melo committed
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 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952
                    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();

        BasicProposalItem *item = new QmlJSAssistProposalItem;
        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);
953
    QString fileName;
Christian Kamm's avatar
Christian Kamm committed
954
    if (url.scheme().compare(QLatin1String("file"), Qt::CaseInsensitive) == 0) {
955 956 957 958 959 960 961 962 963 964
        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
965
        return false;
966
    }
Leandro Melo's avatar
Leandro Melo committed
967 968 969 970 971 972 973

    return completeFileName(relativeBasePath, fileName);
}

// ------------------------------
// QmlJSCompletionAssistInterface
// ------------------------------
974
QmlJSCompletionAssistInterface::QmlJSCompletionAssistInterface(QTextDocument *textDocument,
Leandro Melo's avatar
Leandro Melo committed
975
                                                               int position,
976
                                                               const QString &fileName,
Leandro Melo's avatar
Leandro Melo committed
977 978
                                                               TextEditor::AssistReason reason,
                                                               const SemanticInfo &info)
979
    : DefaultAssistInterface(textDocument, position, fileName, reason)
Leandro Melo's avatar
Leandro Melo committed
980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017
    , 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 {

struct QmlJSLessThan
{
    bool operator() (const BasicProposalItem *a, const BasicProposalItem *b)
    {
        if (a->order() != b->order())
            return a->order() > b->order();
        else if (a->text().isEmpty())
            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;
        return a->text() < b->text();
    }
};

} // Anonymous

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

1031
void QmlJSAssistProposalModel::sort(const QString &)
Leandro Melo's avatar
Leandro Melo committed
1032 1033 1034
{
    qSort(currentItems().first, currentItems().second, QmlJSLessThan());
}
1035 1036 1037 1038 1039

bool QmlJSAssistProposalModel::keepPerfectMatch(TextEditor::AssistReason reason) const
{
    return reason == ExplicitlyInvoked;
}
1040 1041

} // namespace QmlJSEditor