qmljscompletionassist.cpp 36.6 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 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

#include <utils/qtcassert.h>
45
#include <utils/qtcoverride.h>
Leandro Melo's avatar
Leandro Melo committed
46 47 48 49

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

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

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

71 72 73 74
namespace QmlJSEditor {

using namespace Internal;

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

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

86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
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;
};

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

125 126 127 128 129 130 131 132 133 134 135 136 137
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)
    {}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

338 339 340 341 342
} // namesapce QmlJSEditor

Q_DECLARE_METATYPE(QmlJSEditor::CompleteFunctionCall)

namespace QmlJSEditor {
343

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

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

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

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

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

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

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

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

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

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

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

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

QmlJSCompletionAssistProcessor::~QmlJSCompletionAssistProcessor()
{}

IAssistProposal *QmlJSCompletionAssistProcessor::createContentProposal() const
{
    IGenericProposalModel *model = new QmlJSAssistProposalModel(m_completions);
    IAssistProposal *proposal = new GenericProposal(m_startPosition, model);
    return proposal;
}

519 520 521
IAssistProposal *QmlJSCompletionAssistProcessor::createHintProposal(
        const QString &functionName, const QStringList &namedArguments,
        int optionalNamedArguments, bool isVariadic) const
Leandro Melo's avatar
Leandro Melo committed
522
{
523 524
    IFunctionHintProposalModel *model = new FunctionHintProposalModel(
                functionName, namedArguments, optionalNamedArguments, isVariadic);
Leandro Melo's avatar
Leandro Melo committed
525 526 527 528 529 530 531 532 533 534 535
    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;

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

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

    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;

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

562 563 564 565 566
    // 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
567 568
    QChar completionOperator;
    if (m_startPosition > 0)
569
        completionOperator = m_interface->textDocument()->characterAt(m_startPosition - 1);
Leandro Melo's avatar
Leandro Melo committed
570

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

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

        // 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
629 630 631 632 633 634 635 636 637 638
        literalText = literalText.mid(1);

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

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

        // ### enum completion?

        return 0;
651
    }
652 653

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

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

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

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

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

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

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

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

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

753 754 755 756
            ProcessProperties processProperties(&scopeChain);
            processProperties.setGlobalCompletion(true);
            processProperties.setEnumerateGeneratedSlots(true);
            processProperties.setEnumerateSlots(false);
Leandro Melo's avatar
Leandro Melo committed
757 758 759 760 761 762 763 764

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

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

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

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

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

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

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

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

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

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

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

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

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

    return 0;
}

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

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

            int startPos = cursorPos - 1;
            for (; startPos != -1; --startPos) {
870
                if (!isIdentifierChar(m_interface->textDocument()->characterAt(startPos)))
Leandro Melo's avatar
Leandro Melo committed
871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886
                    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) {
887
        QTextCursor tc(m_interface->textDocument());
Leandro Melo's avatar
Leandro Melo committed
888 889 890 891 892 893 894 895 896 897 898 899
        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
900
                if (tk.is(Token::Comment) || tk.is(Token::String) || tk.is(Token::RegExp))
Leandro Melo's avatar
Leandro Melo committed
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 933 934 935 936 937 938 939 940 941 942 943 944 945 946
                    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);
947
    QString fileName;
Christian Kamm's avatar
Christian Kamm committed
948
    if (url.scheme().compare(QLatin1String("file"), Qt::CaseInsensitive) == 0) {
949 950 951 952 953 954 955 956 957 958
        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
959
        return false;
960
    }
Leandro Melo's avatar
Leandro Melo committed
961 962 963 964 965 966 967

    return completeFileName(relativeBasePath, fileName);
}

// ------------------------------
// QmlJSCompletionAssistInterface
// ------------------------------
968
QmlJSCompletionAssistInterface::QmlJSCompletionAssistInterface(QTextDocument *textDocument,
Leandro Melo's avatar
Leandro Melo committed
969
                                                               int position,
970
                                                               const QString &fileName,
Leandro Melo's avatar
Leandro Melo committed
971 972
                                                               TextEditor::AssistReason reason,
                                                               const SemanticInfo &info)
973
    : DefaultAssistInterface(textDocument, position, fileName, reason)
Leandro Melo's avatar
Leandro Melo committed
974 975 976 977 978 979 980 981 982 983 984 985 986
    , 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 {

987
class QmlJSLessThan
Leandro Melo's avatar
Leandro Melo committed
988
{
989 990 991
public:
    QmlJSLessThan(const QString &searchString) : m_searchString(searchString)
    { }
Leandro Melo's avatar
Leandro Melo committed
992 993 994 995
    bool operator() (const BasicProposalItem *a, const BasicProposalItem *b)
    {
        if (a->order() != b->order())
            return a->order() > b->order();
996
        else if (a->text().isEmpty() && ! b->text().isEmpty())
Leandro Melo's avatar
Leandro Melo committed
997 998 999 1000 1001 1002 1003 1004 1005
            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;
1006 1007 1008 1009
        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
1010 1011
        return a->text() < b->text();
    }
1012 1013
private:
    QString m_searchString;
Leandro Melo's avatar
Leandro Melo committed
1014 1015 1016 1017 1018 1019 1020
};

} // Anonymous

// -------------------------
// QmlJSAssistProposalModel
// -------------------------
1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033
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;
}

1034
void QmlJSAssistProposalModel::sort(const QString &prefix)
Leandro Melo's avatar
Leandro Melo committed
1035
{
1036
    std::sort(currentItems().first, currentItems().second, QmlJSLessThan(prefix));
Leandro Melo's avatar
Leandro Melo committed
1037
}
1038 1039 1040 1041 1042

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

} // namespace QmlJSEditor