Files
qt-creator/src/plugins/texteditor/codeassist/codeassistant.cpp

513 lines
16 KiB
C++
Raw Normal View History

/**************************************************************************
**
** This file is part of Qt Creator
**
** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies).
**
** Contact: Nokia Corporation (info@qt.nokia.com)
**
**
** 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.
**
** If you have questions regarding the use of this file, please contact
** Nokia at info@qt.nokia.com.
**
**************************************************************************/
#include "codeassistant.h"
#include "completionassistprovider.h"
#include "quickfixassistprovider.h"
#include "iassistprocessor.h"
#include "iassistproposal.h"
#include "iassistproposalwidget.h"
#include "iassistinterface.h"
#include "iassistproposalitem.h"
#include "runner.h"
#include <texteditor/basetexteditor.h>
#include <texteditor/texteditorsettings.h>
#include <texteditor/completionsettings.h>
#include <extensionsystem/pluginmanager.h>
#include <QtCore/QObject>
#include <QtCore/QList>
#include <QtCore/QTimer>
#include <QtCore/QDebug>
#include <QtGui/QKeyEvent>
using namespace TextEditor;
using namespace Internal;
namespace {
template <class T>
void filterEditorSpecificProviders(QList<T *> *providers, const QString &editorId)
{
typename QList<T *>::iterator it = providers->begin();
while (it != providers->end()) {
if ((*it)->supportsEditor(editorId))
++it;
else
it = providers->erase(it);
}
}
} // Anonymous
namespace TextEditor {
class CodeAssistantPrivate : public QObject
{
Q_OBJECT
public:
CodeAssistantPrivate(CodeAssistant *assistant);
virtual ~CodeAssistantPrivate();
void configure(BaseTextEditor *textEditor);
bool isConfigured() const;
void invoke(AssistKind kind, IAssistProvider *provider = 0);
void process();
void requestProposal(AssistReason reason, AssistKind kind, IAssistProvider *provider = 0);
void cancelCurrentRequest();
void invalidateCurrentRequestData();
void displayProposal(IAssistProposal *newProposal, AssistReason reason);
bool isDisplayingProposal() const;
bool isWaitingForProposal() const;
void notifyChange();
bool hasContext() const;
void destroyContext();
CompletionAssistProvider *identifyActivationSequence();
void stopAutomaticProposalTimer();
void startAutomaticProposalTimer();
virtual bool eventFilter(QObject *o, QEvent *e);
private slots:
void finalizeRequest();
void proposalComputed();
void processProposalItem(IAssistProposalItem *proposalItem);
void handlePrefixExpansion(const QString &newPrefix);
void finalizeProposal();
void automaticProposalTimeout();
void updateCompletionSettings(const TextEditor::CompletionSettings &settings);
private:
CodeAssistant *m_q;
BaseTextEditor *m_textEditor;
QList<CompletionAssistProvider *> m_completionProviders;
QList<QuickFixAssistProvider *> m_quickFixProviders;
Internal::ProcessorRunner *m_requestRunner;
CompletionAssistProvider *m_requestProvider;
AssistKind m_assistKind;
IAssistProposalWidget *m_proposalWidget;
QScopedPointer<IAssistProposal> m_proposal;
bool m_proposalApplied;
bool m_receivedContentWhileWaiting;
QTimer m_automaticProposalTimer;
CompletionSettings m_settings;
static const QChar m_null;
};
} // TextEditor
// --------------------
// CodeAssistantPrivate
// --------------------
const QChar CodeAssistantPrivate::m_null;
CodeAssistantPrivate::CodeAssistantPrivate(CodeAssistant *assistant)
: m_q(assistant)
, m_textEditor(0)
, m_requestRunner(0)
, m_requestProvider(0)
, m_proposalWidget(0)
, m_proposalApplied(false)
, m_receivedContentWhileWaiting(false)
, m_settings(TextEditorSettings::instance()->completionSettings())
{
m_automaticProposalTimer.setSingleShot(true);
m_automaticProposalTimer.setInterval(250);
connect(&m_automaticProposalTimer, SIGNAL(timeout()), this, SLOT(automaticProposalTimeout()));
connect(TextEditorSettings::instance(),
SIGNAL(completionSettingsChanged(TextEditor::CompletionSettings)),
this,
SLOT(updateCompletionSettings(TextEditor::CompletionSettings)));
}
CodeAssistantPrivate::~CodeAssistantPrivate()
{}
void CodeAssistantPrivate::configure(BaseTextEditor *textEditor)
{
// @TODO: There's a list of providers but currently only the first one is used. Perhaps we
// should implement a truly mechanism to support multiple providers for an editor (either
// merging or not proposals) or just leave it as not extensible and store directly the one
// completion and quick-fix provider (getting rid of the list).
m_textEditor = textEditor;
m_completionProviders =
ExtensionSystem::PluginManager::instance()->getObjects<CompletionAssistProvider>();
filterEditorSpecificProviders(&m_completionProviders, m_textEditor->id());
m_quickFixProviders =
ExtensionSystem::PluginManager::instance()->getObjects<QuickFixAssistProvider>();
filterEditorSpecificProviders(&m_quickFixProviders, m_textEditor->id());
m_textEditor->editorWidget()->installEventFilter(this);
}
bool CodeAssistantPrivate::isConfigured() const
{
return m_textEditor != 0;
}
void CodeAssistantPrivate::invoke(AssistKind kind, IAssistProvider *provider)
{
if (!isConfigured())
return;
stopAutomaticProposalTimer();
if (isDisplayingProposal() && m_assistKind == kind && !m_proposal->isFragile()) {
m_proposalWidget->setReason(ExplicitlyInvoked);
} else {
destroyContext();
requestProposal(ExplicitlyInvoked, kind, provider);
}
}
void CodeAssistantPrivate::process()
{
if (!isConfigured())
return;
stopAutomaticProposalTimer();
if (m_settings.m_completionTrigger != ManualCompletion) {
if (CompletionAssistProvider *provider = identifyActivationSequence()) {
if (isWaitingForProposal())
cancelCurrentRequest();
requestProposal(ActivationCharacter, Completion, provider);
return;
}
}
startAutomaticProposalTimer();
}
void CodeAssistantPrivate::requestProposal(AssistReason reason,
AssistKind kind,
IAssistProvider *provider)
{
Q_ASSERT(!isWaitingForProposal());
if (!provider) {
if (kind == Completion) {
if (!m_completionProviders.isEmpty())
provider = m_completionProviders.at(0);
} else if (!m_quickFixProviders.isEmpty()) {
provider = m_quickFixProviders.at(0);
}
if (!provider)
return;
}
m_assistKind = kind;
IAssistProcessor *processor = provider->createProcessor();
IAssistInterface *assistInterface =
m_textEditor->editorWidget()->createAssistInterface(kind, reason);
if (!assistInterface)
return;
if (kind == Completion) {
CompletionAssistProvider *completionProvider =
static_cast<CompletionAssistProvider *>(provider);
if (completionProvider->isAsynchronous()) {
m_requestProvider = completionProvider;
m_requestRunner = new ProcessorRunner;
connect(m_requestRunner, SIGNAL(finished()), this, SLOT(proposalComputed()));
connect(m_requestRunner, SIGNAL(finished()), this, SLOT(finalizeRequest()));
assistInterface->detach(m_requestRunner);
m_requestRunner->setReason(reason);
m_requestRunner->setProcessor(processor);
m_requestRunner->setAssistInterface(assistInterface);
m_requestRunner->start();
return;
}
}
IAssistProposal *newProposal = processor->perform(assistInterface);
displayProposal(newProposal, reason);
delete processor;
}
void CodeAssistantPrivate::cancelCurrentRequest()
{
m_requestRunner->setDiscardProposal(true);
disconnect(m_requestRunner, SIGNAL(finished()), this, SLOT(proposalComputed()));
invalidateCurrentRequestData();
}
void CodeAssistantPrivate::proposalComputed()
{
// Since the request runner is a different thread, there's still a gap in which the queued
// signal could be processed after an invalidation of the current request.
if (!m_requestRunner)
return;
IAssistProposal *newProposal = m_requestRunner->proposal();
AssistReason reason = m_requestRunner->reason();
invalidateCurrentRequestData();
displayProposal(newProposal, reason);
}
void CodeAssistantPrivate::displayProposal(IAssistProposal *newProposal, AssistReason reason)
{
if (!newProposal)
return;
QScopedPointer<IAssistProposal> proposalCandidate(newProposal);
if (isDisplayingProposal()) {
if (!m_proposal->isFragile() || proposalCandidate->isFragile())
return;
destroyContext();
}
if (m_textEditor->position() < proposalCandidate->basePosition())
return;
m_proposal.reset(proposalCandidate.take());
if (m_proposal->isCorrective())
m_proposal->makeCorrection(m_textEditor);
m_proposalWidget = m_proposal->createWidget();
connect(m_proposalWidget, SIGNAL(destroyed()), this, SLOT(finalizeProposal()));
connect(m_proposalWidget, SIGNAL(prefixExpanded(QString)),
this, SLOT(handlePrefixExpansion(QString)));
connect(m_proposalWidget, SIGNAL(proposalItemActivated(IAssistProposalItem*)),
this, SLOT(processProposalItem(IAssistProposalItem*)));
m_proposalWidget->setAssistant(m_q);
m_proposalWidget->setReason(reason);
m_proposalWidget->setUnderlyingWidget(m_textEditor->widget());
m_proposalWidget->setModel(m_proposal->model());
m_proposalWidget->setDisplayRect(m_textEditor->cursorRect(m_proposal->basePosition()));
if (m_receivedContentWhileWaiting)
m_proposalWidget->setIsSynchronized(false);
else
m_proposalWidget->setIsSynchronized(true);
m_proposalWidget->showProposal(m_textEditor->textAt(
m_proposal->basePosition(),
m_textEditor->position() - m_proposal->basePosition()));
}
void CodeAssistantPrivate::processProposalItem(IAssistProposalItem *proposalItem)
{
proposalItem->apply(m_textEditor, m_proposal->basePosition());
destroyContext();
process();
}
void CodeAssistantPrivate::handlePrefixExpansion(const QString &newPrefix)
{
const int currentPosition = m_textEditor->position();
m_textEditor->setCursorPosition(m_proposal->basePosition());
m_textEditor->replace(currentPosition - m_proposal->basePosition(), newPrefix);
notifyChange();
}
void CodeAssistantPrivate::finalizeRequest()
{
if (ProcessorRunner *runner = qobject_cast<ProcessorRunner *>(sender()))
delete runner;
}
void CodeAssistantPrivate::finalizeProposal()
{
m_proposal.reset();
m_proposalWidget = 0;
if (m_receivedContentWhileWaiting)
m_receivedContentWhileWaiting = false;
}
bool CodeAssistantPrivate::isDisplayingProposal() const
{
return m_proposalWidget != 0;
}
bool CodeAssistantPrivate::isWaitingForProposal() const
{
return m_requestRunner != 0;
}
void CodeAssistantPrivate::invalidateCurrentRequestData()
{
m_requestRunner = 0;
m_requestProvider = 0;
}
CompletionAssistProvider *CodeAssistantPrivate::identifyActivationSequence()
{
for (int i = 0; i < m_completionProviders.size(); ++i) {
CompletionAssistProvider *provider = m_completionProviders.at(i);
const int length = provider->activationCharSequenceLength();
if (length == 0)
continue;
QString sequence = m_textEditor->textAt(m_textEditor->position() - length, length);
const int lengthDiff = length - sequence.length();
for (int j = 0; j < lengthDiff; ++j)
sequence.prepend(m_null);
if (provider->isActivationCharSequence(sequence))
return provider;
}
return 0;
}
void CodeAssistantPrivate::notifyChange()
{
stopAutomaticProposalTimer();
if (isDisplayingProposal()) {
if (m_textEditor->position() < m_proposal->basePosition())
destroyContext();
else
m_proposalWidget->updateProposal(
m_textEditor->textAt(m_proposal->basePosition(),
m_textEditor->position() - m_proposal->basePosition()));
}
}
bool CodeAssistantPrivate::hasContext() const
{
return m_requestRunner || m_proposalWidget;
}
void CodeAssistantPrivate::destroyContext()
{
stopAutomaticProposalTimer();
if (isWaitingForProposal()) {
cancelCurrentRequest();
} else if (isDisplayingProposal()) {
m_proposalWidget->closeProposal();
disconnect(m_proposalWidget, SIGNAL(destroyed()), this, SLOT(finalizeProposal()));
finalizeProposal();
}
}
void CodeAssistantPrivate::startAutomaticProposalTimer()
{
if (m_settings.m_completionTrigger == AutomaticCompletion)
m_automaticProposalTimer.start();
}
void CodeAssistantPrivate::automaticProposalTimeout()
{
if (isWaitingForProposal() || (isDisplayingProposal() && !m_proposal->isFragile()))
return;
requestProposal(IdleEditor, Completion);
}
void CodeAssistantPrivate::stopAutomaticProposalTimer()
{
if (m_automaticProposalTimer.isActive())
m_automaticProposalTimer.stop();
}
void CodeAssistantPrivate::updateCompletionSettings(const TextEditor::CompletionSettings &settings)
{
m_settings = settings;
}
bool CodeAssistantPrivate::eventFilter(QObject *o, QEvent *e)
{
Q_UNUSED(o);
if (isWaitingForProposal()) {
QEvent::Type type = e->type();
if (type == QEvent::FocusOut) {
destroyContext();
} else if (type == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
const QString &keyText = keyEvent->text();
if ((keyText.isEmpty()
&& keyEvent->key() != Qt::LeftArrow
&& keyEvent->key() != Qt::RightArrow
&& keyEvent->key() != Qt::Key_Shift)
|| (!keyText.isEmpty() &&
!m_requestProvider->isContinuationChar(keyText.at(0)))) {
destroyContext();
} else if (!keyText.isEmpty() && !m_receivedContentWhileWaiting) {
m_receivedContentWhileWaiting = true;
}
}
}
return false;
}
// -------------
// CodeAssistant
// -------------
CodeAssistant::CodeAssistant() : m_d(new CodeAssistantPrivate(this))
{}
CodeAssistant::~CodeAssistant()
{}
void CodeAssistant::configure(BaseTextEditor *textEditor)
{
m_d->configure(textEditor);
}
void CodeAssistant::process()
{
m_d->process();
}
void CodeAssistant::notifyChange()
{
m_d->notifyChange();
}
bool CodeAssistant::hasContext() const
{
return m_d->hasContext();
}
void CodeAssistant::destroyContext()
{
m_d->destroyContext();
}
void CodeAssistant::invoke(AssistKind kind, IAssistProvider *provider)
{
m_d->invoke(kind, provider);
}
#include "codeassistant.moc"