qmljscompletionassist.cpp 36.2 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 513 514 515 516 517 518 519
{}

QmlJSCompletionAssistProcessor::~QmlJSCompletionAssistProcessor()
{}

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

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

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

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

    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;

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

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

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

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

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

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

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

        // ### enum completion?

        return 0;
652
    }
653 654

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return 0;
}

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

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

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

    return completeFileName(relativeBasePath, fileName);
}

// ------------------------------
// QmlJSCompletionAssistInterface
// ------------------------------
969
QmlJSCompletionAssistInterface::QmlJSCompletionAssistInterface(QTextDocument *textDocument,
Leandro Melo's avatar
Leandro Melo committed
970
                                                               int position,
971
                                                               const QString &fileName,
Leandro Melo's avatar
Leandro Melo committed
972 973
                                                               TextEditor::AssistReason reason,
                                                               const SemanticInfo &info)
974
    : DefaultAssistInterface(textDocument, position, fileName, reason)
Leandro Melo's avatar
Leandro Melo committed
975 976 977 978 979 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
    , 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
// -------------------------
1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025
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;
}

1026
void QmlJSAssistProposalModel::sort(const QString &)
Leandro Melo's avatar
Leandro Melo committed
1027 1028 1029
{
    qSort(currentItems().first, currentItems().second, QmlJSLessThan());
}
1030 1031 1032 1033 1034

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

} // namespace QmlJSEditor