Files
qt-creator/src/plugins/languageclient/languageclientcodeassist.cpp
David Schulz 727ea78a5d LSP: restrict automatic completion
Only request an automatic completion if the cursor is after at least 3
'identifier' characters (alphanumerical characters and underscore).

Change-Id: I1bac8f184042fb68135ce24c20d17fc134aae5ac
Reviewed-by: Christian Stenger <christian.stenger@qt.io>
2018-10-16 13:44:14 +00:00

359 lines
13 KiB
C++

/****************************************************************************
**
** Copyright (C) 2018 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** 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 The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#include "languageclientcodeassist.h"
#include "baseclient.h"
#include <languageserverprotocol/completion.h>
#include <texteditor/codeassist/assistinterface.h>
#include <texteditor/codeassist/assistproposalitem.h>
#include <texteditor/codeassist/iassistprocessor.h>
#include <texteditor/codeassist/genericproposal.h>
#include <texteditor/codeassist/genericproposalmodel.h>
#include <utils/algorithm.h>
#include <utils/textutils.h>
#include <utils/utilsicons.h>
#include <QDebug>
#include <QLoggingCategory>
#include <QRegExp>
#include <QTextBlock>
#include <QTextDocument>
#include <QTime>
static Q_LOGGING_CATEGORY(LOGLSPCOMPLETION, "qtc.languageclient.completion", QtWarningMsg);
using namespace LanguageServerProtocol;
namespace LanguageClient {
class LanguageClientCompletionItem : public TextEditor::AssistProposalItemInterface
{
public:
LanguageClientCompletionItem(CompletionItem item);
// AssistProposalItemInterface interface
QString text() const override;
bool implicitlyApplies() const override;
bool prematurelyApplies(const QChar &typedCharacter) const override;
void apply(TextEditor::TextDocumentManipulatorInterface &manipulator, int basePosition) const override;
QIcon icon() const override;
QString detail() const override;
bool isSnippet() const override;
bool isValid() const override;
quint64 hash() const override;
const QString &sortText() const;
bool operator <(const LanguageClientCompletionItem &other) const;
private:
CompletionItem m_item;
mutable QString m_sortText;
};
LanguageClientCompletionItem::LanguageClientCompletionItem(CompletionItem item)
: m_item(std::move(item))
{ }
QString LanguageClientCompletionItem::text() const
{ return m_item.label(); }
bool LanguageClientCompletionItem::implicitlyApplies() const
{ return false; }
bool LanguageClientCompletionItem::prematurelyApplies(const QChar &/*typedCharacter*/) const
{ return false; }
static void applyTextEdit(TextEditor::TextDocumentManipulatorInterface &manipulator,
const TextEdit &edit)
{
using namespace Utils::Text;
const Range range = edit.range();
const QTextDocument *doc = manipulator.textCursorAt(manipulator.currentPosition()).document();
const int start = positionInText(doc, range.start().line() + 1, range.start().character() + 1);
const int end = positionInText(doc, range.end().line() + 1, range.end().character() + 1);
manipulator.replace(start, end - start, edit.newText());
}
void LanguageClientCompletionItem::apply(TextEditor::TextDocumentManipulatorInterface &manipulator,
int /*basePosition*/) const
{
const int pos = manipulator.currentPosition();
if (auto edit = m_item.textEdit()) {
applyTextEdit(manipulator, *edit);
} else {
const QString textToInsert(m_item.insertText().value_or(text()));
int length = 0;
for (auto it = textToInsert.crbegin(); it != textToInsert.crend(); ++it) {
auto ch = *it;
if (ch == manipulator.characterAt(pos - length - 1))
++length;
else if (length != 0)
length = 0;
}
manipulator.replace(pos - length, length, textToInsert);
}
if (auto additionalEdits = m_item.additionalTextEdits()) {
for (const auto &edit : *additionalEdits)
applyTextEdit(manipulator, edit);
}
}
QIcon LanguageClientCompletionItem::icon() const
{
QIcon icon;
using namespace Utils::CodeModelIcon;
const int kind = m_item.kind().value_or(CompletionItemKind::Text);
switch (kind) {
case CompletionItemKind::Method:
case CompletionItemKind::Function:
case CompletionItemKind::Constructor: icon = iconForType(FuncPublic); break;
case CompletionItemKind::Field: icon = iconForType(VarPublic); break;
case CompletionItemKind::Variable: icon = iconForType(VarPublic); break;
case CompletionItemKind::Class: icon = iconForType(Class); break;
case CompletionItemKind::Module: icon = iconForType(Namespace); break;
case CompletionItemKind::Property: icon = iconForType(Property); break;
case CompletionItemKind::Enum: icon = iconForType(Enum); break;
case CompletionItemKind::Keyword: icon = iconForType(Keyword); break;
case CompletionItemKind::Snippet: icon = QIcon(":/texteditor/images/snippet.png"); break;
case CompletionItemKind::EnumMember: icon = iconForType(Enumerator); break;
case CompletionItemKind::Struct: icon = iconForType(Struct); break;
default:
break;
}
return icon;
}
QString LanguageClientCompletionItem::detail() const
{
if (auto _doc = m_item.documentation()) {
auto doc = *_doc;
QString detailDocText;
if (Utils::holds_alternative<QString>(doc)) {
detailDocText = Utils::get<QString>(doc);
} else if (Utils::holds_alternative<MarkupContent>(doc)) {
// TODO markdown parser?
detailDocText = Utils::get<MarkupContent>(doc).content();
}
if (!detailDocText.isEmpty())
return detailDocText;
}
return m_item.detail().value_or(text());
}
bool LanguageClientCompletionItem::isSnippet() const
{
// FIXME add lsp > creator snippet converter
// return m_item.insertTextFormat().value_or(CompletionItem::PlainText);
return false;
}
bool LanguageClientCompletionItem::isValid() const
{
return m_item.isValid(nullptr);
}
quint64 LanguageClientCompletionItem::hash() const
{
return qHash(m_item.label()); // TODO: naaaa
}
const QString &LanguageClientCompletionItem::sortText() const
{
if (m_sortText.isEmpty())
m_sortText = m_item.sortText().has_value() ? *m_item.sortText() : m_item.label();
return m_sortText;
}
bool LanguageClientCompletionItem::operator <(const LanguageClientCompletionItem &other) const
{
return sortText() < other.sortText();
}
class LanguageClientCompletionModel : public TextEditor::GenericProposalModel
{
public:
// GenericProposalModel interface
bool isSortable(const QString &/*prefix*/) const override { return true; }
void sort(const QString &/*prefix*/) override;
bool supportsPrefixExpansion() const override { return false; }
};
void LanguageClientCompletionModel::sort(const QString &/*prefix*/)
{
using namespace TextEditor;
std::sort(m_currentItems.begin(), m_currentItems.end(),
[] (AssistProposalItemInterface *a, AssistProposalItemInterface *b){
return *(dynamic_cast<LanguageClientCompletionItem *>(a)) < *(
dynamic_cast<LanguageClientCompletionItem *>(b));
});
}
class LanguageClientCompletionAssistProcessor : public TextEditor::IAssistProcessor
{
public:
LanguageClientCompletionAssistProcessor(BaseClient *client);
TextEditor::IAssistProposal *perform(const TextEditor::AssistInterface *interface) override;
bool running() override;
private:
void handleCompletionResponse(const Response<CompletionResult, LanguageClientNull> &response);
QPointer<BaseClient> m_client;
bool m_running = false;
int m_pos = -1;
};
LanguageClientCompletionAssistProcessor::LanguageClientCompletionAssistProcessor(BaseClient *client)
: m_client(client)
{ }
static QString assistReasonString(TextEditor::AssistReason reason)
{
switch (reason) {
case TextEditor::IdleEditor: return QString("idle editor");
case TextEditor::ActivationCharacter: return QString("activation character");
case TextEditor::ExplicitlyInvoked: return QString("explicitly invoking");
}
return QString("unknown reason");
}
TextEditor::IAssistProposal *LanguageClientCompletionAssistProcessor::perform(
const TextEditor::AssistInterface *interface)
{
QTC_ASSERT(m_client, return nullptr);
m_pos = interface->position();
if (interface->reason() == TextEditor::IdleEditor) {
// Trigger an automatic completion request only when we are on a word with more than 2 "identifier" character
const QRegExp regexp("[_a-zA-Z0-9]*");
int delta = 0;
while (m_pos - delta > 0 && regexp.exactMatch(interface->textAt(m_pos - delta - 1, delta + 1)))
++delta;
if (delta < 3)
return nullptr;
}
CompletionRequest completionRequest;
CompletionParams::CompletionContext context;
context.setTriggerKind(interface->reason() == TextEditor::ActivationCharacter
? CompletionParams::TriggerCharacter
: CompletionParams::Invoked);
auto params = completionRequest.params().value_or(CompletionParams());
int line;
int column;
if (!Utils::Text::convertPosition(interface->textDocument(), m_pos, &line, &column))
return nullptr;
--line; // line is 0 based in the protocol
params.setPosition({line, column});
params.setTextDocument(
DocumentUri::fromFileName(Utils::FileName::fromString(interface->fileName())));
completionRequest.setResponseCallback([this](auto response) {
this->handleCompletionResponse(response);
});
completionRequest.setParams(params);
m_client->sendContent(completionRequest);
m_running = true;
qCDebug(LOGLSPCOMPLETION) << QTime::currentTime()
<< " : request completions at " << m_pos
<< " by " << assistReasonString(interface->reason());
return nullptr;
}
bool LanguageClientCompletionAssistProcessor::running()
{
return m_running;
}
void LanguageClientCompletionAssistProcessor::handleCompletionResponse(
const Response<CompletionResult, LanguageClientNull> &response)
{
using namespace TextEditor;
qCDebug(LOGLSPCOMPLETION) << QTime::currentTime() << " : got completions";
m_running = false;
QTC_ASSERT(m_client, return);
if (auto error = response.error()) {
m_client->log(error.value().message());
return;
}
const Utils::optional<CompletionResult> &result = response.result();
if (!result || Utils::holds_alternative<std::nullptr_t>(*result))
return;
QList<CompletionItem> items;
if (Utils::holds_alternative<CompletionList>(*result)) {
const auto &list = Utils::get<CompletionList>(*result);
items = list.items().value_or(QList<CompletionItem>());
} else if (Utils::holds_alternative<QList<CompletionItem>>(*result)) {
items = Utils::get<QList<CompletionItem>>(*result);
}
auto model = new LanguageClientCompletionModel();
model->loadContent(Utils::transform(items, [](const CompletionItem &item){
return static_cast<AssistProposalItemInterface *>(new LanguageClientCompletionItem(item));
}));
auto proposal = new GenericProposal(m_pos, GenericProposalModelPtr(model));
proposal->setFragile(true);
setAsyncProposalAvailable(proposal);
qCDebug(LOGLSPCOMPLETION) << QTime::currentTime() << " : "
<< items.count() << " completions handled";
}
LanguageClientCompletionAssistProvider::LanguageClientCompletionAssistProvider(BaseClient *client)
: m_client(client)
{ }
TextEditor::IAssistProcessor *LanguageClientCompletionAssistProvider::createProcessor() const
{
return new LanguageClientCompletionAssistProcessor(m_client);
}
TextEditor::IAssistProvider::RunType LanguageClientCompletionAssistProvider::runType() const
{
return TextEditor::IAssistProvider::Asynchronous;
}
int LanguageClientCompletionAssistProvider::activationCharSequenceLength() const
{
return m_activationCharSequenceLength;
}
bool LanguageClientCompletionAssistProvider::isActivationCharSequence(const QString &sequence) const
{
return Utils::anyOf(m_triggerChars, [sequence](const QString &trigger){
return trigger.endsWith(sequence);
});
}
void LanguageClientCompletionAssistProvider::setTriggerCharacters(QList<QString> triggerChars)
{
m_triggerChars = triggerChars;
for (const QString &trigger : triggerChars) {
if (trigger.length() > m_activationCharSequenceLength)
m_activationCharSequenceLength = trigger.length();
}
}
} // namespace LanguageClient