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

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

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

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

#include <utils/qtcassert.h>

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

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

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

namespace {

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

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

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

121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
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)
137 138 139 140
        QVariant data;
        if (const FunctionValue *func = value->asFunctionValue()) {
            // constructors usually also have other interesting members,
            // don't consider them pure functions and complete the '()'
141
            if (!func->lookupMember(QLatin1String("prototype"), 0, 0, false))
142 143 144
                data = QVariant::fromValue(CompleteFunctionCall(func->namedArgumentCount() || func->isVariadic()));
        }
        addCompletion(completions, name, icon, order, data);
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
    }

    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
164
        const CppComponentValue *qmlBase = value_cast<CppComponentValue>(base);
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185

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

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

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

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

216 217 218 219 220
    void setEnumerateSlots(bool enumerate)
    {
        _enumerateSlots = enumerate;
    }

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

226
        processProperties(value);
Leandro Melo's avatar
Leandro Melo committed
227 228
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

335 336
Q_DECLARE_METATYPE(CompleteFunctionCall)

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

356 357 358 359 360 361 362 363 364 365 366 367 368 369 370
    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
371 372 373 374 375 376 377 378 379 380 381
    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);
382 383
    if (cursorOffset)
        editor->setCursorPosition(editor->position() + cursorOffset);
Leandro Melo's avatar
Leandro Melo committed
384 385 386 387 388 389 390 391
}

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

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

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

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

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

        prettyMethod += arg;
    }
434 435 436 437 438 439 440
    if (m_isVariadic) {
        if (m_namedArguments.size())
            prettyMethod += QLatin1String(", ");
        prettyMethod += QLatin1String("...");
    }
    if (m_optionalNamedArguments)
        prettyMethod += QLatin1Char(']');
Leandro Melo's avatar
Leandro Melo committed
441 442 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
    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
470
bool QmlJSCompletionAssistProvider::supportsEditor(const Core::Id &editorId) const
Leandro Melo's avatar
Leandro Melo committed
471
{
472
    return editorId == Constants::C_QMLJSEDITOR_ID;
Leandro Melo's avatar
Leandro Melo committed
473 474 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
}

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

QmlJSCompletionAssistProcessor::~QmlJSCompletionAssistProcessor()
{}

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

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

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

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

    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;

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

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

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

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

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

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

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

        // ### enum completion?

        return 0;
645
    }
646 647 648 649 650

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

651
    // member "a.bc<complete>" or function "foo(<complete>" completion
652 653
    if (completionOperator == QLatin1Char('.')
            || (completionOperator == QLatin1Char('(') && !onIdentifier)) {
654 655
        // Look at the expression under cursor.
        //QTextCursor tc = textWidget->textCursor();
656
        QTextCursor tc(qmlInterface->textDocument());
657 658 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
        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);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return 0;
}

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

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

            int startPos = cursorPos - 1;
            for (; startPos != -1; --startPos) {
835
                if (!isIdentifierChar(m_interface->textDocument()->characterAt(startPos)))
Leandro Melo's avatar
Leandro Melo committed
836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851
                    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) {
852
        QTextCursor tc(m_interface->textDocument());
Leandro Melo's avatar
Leandro Melo committed
853 854 855 856 857 858 859 860 861 862 863 864
        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
865
                if (tk.is(Token::Comment) || tk.is(Token::String) || tk.is(Token::RegExp))
Leandro Melo's avatar
Leandro Melo committed
866 867 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
                    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);
912
    QString fileName;
Christian Kamm's avatar
Christian Kamm committed
913
    if (url.scheme().compare(QLatin1String("file"), Qt::CaseInsensitive) == 0) {
914 915 916 917 918 919 920 921 922 923
        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
924
        return false;
925
    }
Leandro Melo's avatar
Leandro Melo committed
926 927 928 929 930 931 932

    return completeFileName(relativeBasePath, fileName);
}

// ------------------------------
// QmlJSCompletionAssistInterface
// ------------------------------
933
QmlJSCompletionAssistInterface::QmlJSCompletionAssistInterface(QTextDocument *textDocument,
Leandro Melo's avatar
Leandro Melo committed
934
                                                               int position,
935
                                                               Core::IDocument *document,
Leandro Melo's avatar
Leandro Melo committed
936 937
                                                               TextEditor::AssistReason reason,
                                                               const SemanticInfo &info)
938
    : DefaultAssistInterface(textDocument, position, document, reason)
Leandro Melo's avatar
Leandro Melo committed
939 940 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
    , 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
// -------------------------
977 978 979 980 981 982 983 984 985 986 987 988 989
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;
}

Leandro Melo's avatar
Leandro Melo committed
990 991 992 993
void QmlJSAssistProposalModel::sort()
{
    qSort(currentItems().first, currentItems().second, QmlJSLessThan());
}
994 995 996 997 998

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