qmljscompletionassist.cpp 33.5 KB
Newer Older
Leandro Melo's avatar
Leandro Melo committed
1 2 3 4
/**************************************************************************
**
** This file is part of Qt Creator
**
hjk's avatar
hjk committed
5
** Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies).
Leandro Melo's avatar
Leandro Melo committed
6
**
Eike Ziller's avatar
Eike Ziller committed
7
** Contact: http://www.qt-project.org/
Leandro Melo's avatar
Leandro Melo committed
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
**
**
** GNU Lesser General Public License Usage
**
** 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, Nokia gives you certain additional
** rights. These rights are described in the Nokia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** Other Usage
**
** Alternatively, this file may be used in accordance with the terms and
** conditions contained in a signed written agreement between you and Nokia.
**
**
**************************************************************************/

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

36
#include <coreplugin/idocument.h>
Leandro Melo's avatar
Leandro Melo committed
37 38 39 40 41

#include <texteditor/codeassist/iassistinterface.h>
#include <texteditor/codeassist/genericproposal.h>
#include <texteditor/codeassist/functionhintproposal.h>
#include <texteditor/codeassist/ifunctionhintproposalmodel.h>
42 43
#include <texteditor/texteditorsettings.h>
#include <texteditor/completionsettings.h>
Leandro Melo's avatar
Leandro Melo committed
44 45 46 47 48 49

#include <utils/qtcassert.h>

#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 55 56
#include <qmljs/qmljsscanner.h>
#include <qmljs/qmljsbind.h>
#include <qmljs/qmljscompletioncontextfinder.h>
#include <qmljs/qmljsscopebuilder.h>

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

using namespace QmlJS;
using namespace QmlJSEditor;
68
using namespace QmlJSTools;
Leandro Melo's avatar
Leandro Melo committed
69 70 71 72 73 74 75 76 77 78 79 80 81 82
using namespace Internal;
using namespace TextEditor;

namespace {

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

83 84 85 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
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;
};

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

122 123 124 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)
    {}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

bool isLiteral(AST::Node *ast)
{
    if (AST::cast<AST::StringLiteral *>(ast))
        return true;
    else if (AST::cast<AST::NumericLiteral *>(ast))
        return true;
    else
        return false;
}

} // Anonymous

337 338
Q_DECLARE_METATYPE(CompleteFunctionCall)

Leandro Melo's avatar
Leandro Melo committed
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
// -----------------------
// 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);

358 359 360 361 362 363 364 365 366 367 368 369 370 371 372
    QString content = text();
    int cursorOffset = 0;

    const CompletionSettings &completionSettings =
            TextEditorSettings::instance()->completionSettings();
    const bool autoInsertBrackets = completionSettings.m_autoInsertBrackets;

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

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

    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;
409 410 411
    QStringList m_namedArguments;
    int m_optionalNamedArguments;
    bool m_isVariadic;
Leandro Melo's avatar
Leandro Melo committed
412 413 414 415 416 417 418 419 420 421
};

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

    QString prettyMethod;
    prettyMethod += QString::fromLatin1("function ");
    prettyMethod += m_functionName;
    prettyMethod += QLatin1Char('(');
422 423 424
    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
425 426 427
        if (i != 0)
            prettyMethod += QLatin1String(", ");

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

        prettyMethod += arg;
    }
436 437 438 439 440 441 442
    if (m_isVariadic) {
        if (m_namedArguments.size())
            prettyMethod += QLatin1String(", ");
        prettyMethod += QLatin1String("...");
    }
    if (m_optionalNamedArguments)
        prettyMethod += QLatin1Char(']');
Leandro Melo's avatar
Leandro Melo committed
443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471
    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
472
bool QmlJSCompletionAssistProvider::supportsEditor(const Core::Id &editorId) const
Leandro Melo's avatar
Leandro Melo committed
473
{
474
    return editorId == Constants::C_QMLJSEDITOR_ID;
Leandro Melo's avatar
Leandro Melo committed
475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514
}

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)
    , m_snippetCollector(Constants::QML_SNIPPETS_GROUP_ID, iconForColor(Qt::red), SnippetOrder)
{}

QmlJSCompletionAssistProcessor::~QmlJSCompletionAssistProcessor()
{}

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

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

532
    const QString &fileName = m_interface->document()->fileName();
Leandro Melo's avatar
Leandro Melo committed
533 534

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

    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;

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

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

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

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

        // 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
625 626 627 628 629 630 631 632 633 634
        literalText = literalText.mid(1);

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

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

        // ### enum completion?

        return 0;
647
    }
648 649 650 651 652

    // currently path-in-stringliteral is the only completion available in imports
    if (contextFinder.isInImport())
        return 0;

653
    // member "a.bc<complete>" or function "foo(<complete>" completion
654 655
    if (completionOperator == QLatin1Char('.')
            || (completionOperator == QLatin1Char('(') && !onIdentifier)) {
656 657
        // Look at the expression under cursor.
        //QTextCursor tc = textWidget->textCursor();
658
        QTextCursor tc(qmlInterface->textDocument());
659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691
        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);

692 693 694
                    QStringList namedArguments;
                    for (int i = 0; i < f->namedArgumentCount(); ++i)
                        namedArguments.append(f->argumentName(i));
695

696 697
                    return createHintProposal(functionName.trimmed(), namedArguments,
                                              f->optionalNamedArgumentCount(), f->isVariadic());
698 699 700 701 702 703 704 705
                }
            }
        }

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

707
    // global completion
708
    if (onIdentifier || assistInterface->reason() == ExplicitlyInvoked) {
Leandro Melo's avatar
Leandro Melo committed
709 710 711 712 713 714 715 716 717 718 719

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

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

720 721 722 723
            ProcessProperties processProperties(&scopeChain);
            processProperties.setGlobalCompletion(true);
            processProperties.setEnumerateGeneratedSlots(true);
            processProperties.setEnumerateSlots(false);
Leandro Melo's avatar
Leandro Melo committed
724 725 726 727 728 729 730 731

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

732 733 734 735 736
            {
                LhsCompletionAdder completionAdder(&m_completions, m_interface->symbolIcon(),
                                                   PropertyOrder, contextFinder.isAfterOnInLhsOfBinding());
                processProperties(qmlScopeType, &completionAdder);
            }
Leandro Melo's avatar
Leandro Melo committed
737 738

            if (ScopeBuilder::isPropertyChangesObject(context, qmlScopeType)
739
                    && scopeChain.qmlScopeObjects().size() == 2) {
740 741
                CompletionAdder completionAdder(&m_completions, m_interface->symbolIcon(), SymbolOrder);
                processProperties(scopeChain.qmlScopeObjects().first(), &completionAdder);
Leandro Melo's avatar
Leandro Melo committed
742 743 744 745 746 747 748
            }
        }

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

            // complete enum values for enum properties
749
            const Value *value =
Leandro Melo's avatar
Leandro Melo committed
750
                    getPropertyValue(qmlScopeType, contextFinder.bindingPropertyName(), context);
751
            if (const QmlEnumValue *enumValue =
Christian Kamm's avatar
Christian Kamm committed
752
                    value_cast<QmlEnumValue>(value)) {
753 754 755 756 757 758 759
                const QString &name = context->imports(document.data())->nameForImportedObject(enumValue->owner(), context.data());
                foreach (const QString &key, enumValue->keys()) {
                    QString completion;
                    if (name.isEmpty())
                        completion = QString("\"%1\"").arg(key);
                    else
                        completion = QString("%1.%2").arg(name, key);
760
                    addCompletion(&m_completions, key, m_interface->symbolIcon(),
761 762
                                  EnumValueOrder, completion);
                }
Leandro Melo's avatar
Leandro Melo committed
763 764 765 766 767 768 769
            }
        }

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

        if (doQmlTypeCompletion) {
770
            if (const ObjectValue *qmlTypes = scopeChain.qmlTypes()) {
771 772 773
                ProcessProperties processProperties(&scopeChain);
                CompletionAdder completionAdder(&m_completions, m_interface->symbolIcon(), TypeOrder);
                processProperties(qmlTypes, &completionAdder);
Leandro Melo's avatar
Leandro Melo committed
774 775 776 777 778
            }
        }

        if (doGlobalCompletion) {
            // It's a global completion.
779 780 781 782
            ProcessProperties processProperties(&scopeChain);
            processProperties.setGlobalCompletion(true);
            CompletionAdder completionAdder(&m_completions, m_interface->symbolIcon(), SymbolOrder);
            processProperties(&completionAdder);
Leandro Melo's avatar
Leandro Melo committed
783 784 785 786
        }

        if (doJsKeywordCompletion) {
            // add js keywords
787
            addCompletions(&m_completions, Scanner::keywords(), m_interface->keywordIcon(), KeywordOrder);
Leandro Melo's avatar
Leandro Melo committed
788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803
        }

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

804
            addCompletions(&m_completions, qmlWords, m_interface->keywordIcon(), KeywordOrder);
Leandro Melo's avatar
Leandro Melo committed
805
            if (!doJsKeywordCompletion)
806
                addCompletions(&m_completions, qmlWordsAlsoInJs, m_interface->keywordIcon(), KeywordOrder);
Leandro Melo's avatar
Leandro Melo committed
807 808
        }

809
        m_completions.append(m_snippetCollector.collect());
Leandro Melo's avatar
Leandro Melo committed
810 811 812 813 814 815 816 817 818 819 820 821 822 823

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

    return 0;
}

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

    bool maybeAccept = false;
824
    const QChar &charBeforeCursor = m_interface->textDocument()->characterAt(cursorPos - 1);
Leandro Melo's avatar
Leandro Melo committed
825 826 827
    if (isActivationChar(charBeforeCursor)) {
        maybeAccept = true;
    } else {
828
        const QChar &charUnderCursor = m_interface->textDocument()->characterAt(cursorPos);
Leandro Melo's avatar
Leandro Melo committed
829 830 831 832 833 834 835 836
        if (isIdentifierChar(charBeforeCursor)
                && ((charUnderCursor.isSpace()
                    || charUnderCursor.isNull()
                    || isDelimiterChar(charUnderCursor))
                || isIdentifierChar(charUnderCursor))) {

            int startPos = cursorPos - 1;
            for (; startPos != -1; --startPos) {
837
                if (!isIdentifierChar(m_interface->textDocument()->characterAt(startPos)))
Leandro Melo's avatar
Leandro Melo committed
838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853
                    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) {
854
        QTextCursor tc(m_interface->textDocument());
Leandro Melo's avatar
Leandro Melo committed
855 856 857 858 859 860 861 862 863 864 865 866
        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
867
                if (tk.is(Token::Comment) || tk.is(Token::String) || tk.is(Token::RegExp))
Leandro Melo's avatar
Leandro Melo committed
868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913
                    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);
914
    QString fileName;
Christian Kamm's avatar
Christian Kamm committed
915
    if (url.scheme().compare(QLatin1String("file"), Qt::CaseInsensitive) == 0) {
916 917 918 919 920 921 922 923 924 925
        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
926
        return false;
927
    }
Leandro Melo's avatar
Leandro Melo committed
928 929 930 931 932 933 934

    return completeFileName(relativeBasePath, fileName);
}

// ------------------------------
// QmlJSCompletionAssistInterface
// ------------------------------
935
QmlJSCompletionAssistInterface::QmlJSCompletionAssistInterface(QTextDocument *textDocument,
Leandro Melo's avatar
Leandro Melo committed
936
                                                               int position,
937
                                                               Core::IDocument *document,
Leandro Melo's avatar
Leandro Melo committed
938 939
                                                               TextEditor::AssistReason reason,
                                                               const SemanticInfo &info)
940
    : DefaultAssistInterface(textDocument, position, document, reason)
Leandro Melo's avatar
Leandro Melo committed
941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982
    , 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
// -------------------------
void QmlJSAssistProposalModel::sort()
{
    qSort(currentItems().first, currentItems().second, QmlJSLessThan());
}
983 984 985 986 987

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