LanguageClient: improve clangd function hint

Add a Clangd specific function hint model that alwys highlights the
current parameter based on the number of commas in front of the cursor
position, like the builtin code model. It also correctly closes the
proposal after typing the closing parenthesis.

Fixes: QTCREATORBUG-26346
Fixes: QTCREATORBUG-30489
Change-Id: I09d3ac6856acfe5e0f206d8c3a96dbb561ea2ce7
Reviewed-by: Christian Kandeler <christian.kandeler@qt.io>
This commit is contained in:
David Schulz
2024-03-05 12:47:33 +01:00
parent 17f40221e0
commit 325db93a7b
11 changed files with 166 additions and 61 deletions

View File

@@ -405,6 +405,7 @@ ClangdClient::ClangdClient(Project *project, const Utils::FilePath &jsonDbDir, c
setSupportedLanguage(langFilter);
setActivateDocumentAutomatically(true);
setCompletionAssistProvider(new ClangdCompletionAssistProvider(this));
setFunctionHintAssistProvider(new ClangdFunctionHintProvider(this));
setQuickFixAssistProvider(new ClangdQuickFixProvider(this));
symbolSupport().setLimitRenamingToProjects(true);
symbolSupport().setRenameResultsEnhancer([](const SearchResultItems &symbolOccurrencesInCode) {

View File

@@ -102,13 +102,52 @@ private:
QElapsedTimer m_timer;
};
class ClangdFunctionHintProposalModel : public FunctionHintProposalModel
{
public:
using FunctionHintProposalModel::FunctionHintProposalModel;
private:
int activeArgument(const QString &prefix) const override
{
const int arg = activeArgumenForPrefix(prefix);
if (arg < 0)
return -1;
m_currentArg = arg;
return arg;
}
QString text(int index) const override
{
using Parameters = QList<ParameterInformation>;
if (index < 0 || m_sigis.signatures().size() <= index)
return {};
const SignatureInformation signature = m_sigis.signatures().at(index);
QString label = signature.label();
const QList<QString> parameters = Utils::transform(signature.parameters().value_or(Parameters()),
&ParameterInformation::label);
if (parameters.size() <= m_currentArg)
return label;
const QString &parameterText = parameters.at(m_currentArg);
const int start = label.indexOf(parameterText);
const int end = start + parameterText.length();
return label.mid(0, start).toHtmlEscaped() + "<b>" + parameterText.toHtmlEscaped() + "</b>"
+ label.mid(end).toHtmlEscaped();
}
mutable int m_currentArg = 0;
};
class ClangdFunctionHintProcessor : public FunctionHintProcessor
{
public:
ClangdFunctionHintProcessor(ClangdClient *client);
ClangdFunctionHintProcessor(ClangdClient *client, int basePosition);
private:
IAssistProposal *perform() override;
IFunctionHintProposalModel *createModel(const SignatureHelp &signatureHelp) const override;
ClangdClient * const m_client;
};
@@ -138,7 +177,8 @@ IAssistProcessor *ClangdCompletionAssistProvider::createProcessor(
switch (contextAnalyzer.completionAction()) {
case ClangCompletionContextAnalyzer::PassThroughToLibClangAfterLeftParen:
qCDebug(clangdLogCompletion) << "creating function hint processor";
return new ClangdFunctionHintProcessor(m_client);
return new ClangdFunctionHintProcessor(m_client,
contextAnalyzer.positionForProposal());
case ClangCompletionContextAnalyzer::CompletePreprocessorDirective:
qCDebug(clangdLogCompletion) << "creating macro processor";
return new CustomAssistProcessor(m_client,
@@ -606,8 +646,8 @@ QList<AssistProposalItemInterface *> ClangdCompletionAssistProcessor::generateCo
return itemGenerator(items);
}
ClangdFunctionHintProcessor::ClangdFunctionHintProcessor(ClangdClient *client)
: FunctionHintProcessor(client)
ClangdFunctionHintProcessor::ClangdFunctionHintProcessor(ClangdClient *client, int basePosition)
: FunctionHintProcessor(client, basePosition)
, m_client(client)
{}
@@ -621,6 +661,12 @@ IAssistProposal *ClangdFunctionHintProcessor::perform()
return FunctionHintProcessor::perform();
}
IFunctionHintProposalModel *ClangdFunctionHintProcessor::createModel(
const SignatureHelp &signatureHelp) const
{
return new ClangdFunctionHintProposalModel(signatureHelp);
}
ClangdCompletionCapabilities::ClangdCompletionCapabilities(const JsonObject &object)
: TextDocumentClientCapabilities::CompletionCapabilities(object)
{
@@ -631,4 +677,18 @@ ClangdCompletionCapabilities::ClangdCompletionCapabilities(const JsonObject &obj
}
}
ClangdFunctionHintProvider::ClangdFunctionHintProvider(ClangdClient *client)
: FunctionHintAssistProvider(client)
, m_client(client)
{}
IAssistProcessor *ClangdFunctionHintProvider::createProcessor(
const AssistInterface *interface) const
{
ClangCompletionContextAnalyzer contextAnalyzer(interface->textDocument(),
interface->position(), false, {});
contextAnalyzer.analyze();
return new ClangdFunctionHintProcessor(m_client, contextAnalyzer.positionForProposal());
}
} // namespace ClangCodeModel::Internal

View File

@@ -1,11 +1,10 @@
#include <languageclient/languageclientcompletionassist.h>
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#pragma once
#include <languageclient/languageclientcompletionassist.h>
#include <languageclient/languageclientfunctionhint.h>
#include <languageserverprotocol/clientcapabilities.h>
namespace TextEditor { class IAssistProcessor; }
@@ -37,4 +36,16 @@ public:
explicit ClangdCompletionCapabilities(const JsonObject &object);
};
class ClangdFunctionHintProvider : public LanguageClient::FunctionHintAssistProvider
{
public:
ClangdFunctionHintProvider(ClangdClient *client);
private:
TextEditor::IAssistProcessor *createProcessor(
const TextEditor::AssistInterface *assistInterface) const override;
ClangdClient * const m_client;
};
} // namespace ClangCodeModel::Internal

View File

@@ -29,6 +29,7 @@
#include <texteditor/blockrange.h>
#include <texteditor/codeassist/assistproposaliteminterface.h>
#include <texteditor/codeassist/genericproposal.h>
#include <texteditor/codeassist/ifunctionhintproposalmodel.h>
#include <texteditor/codeassist/textdocumentmanipulatorinterface.h>
#include <texteditor/semantichighlighter.h>
#include <texteditor/textmark.h>
@@ -1832,12 +1833,12 @@ void ClangdTestCompletion::testFunctionHints()
QVERIFY(proposal);
QVERIFY(hasItem(proposal, "f() -> void"));
QVERIFY(hasItem(proposal, "f(int a) -> void"));
QVERIFY(hasItem(proposal, "f(const QString &s) -> void"));
QVERIFY(hasItem(proposal, "f(char c, int optional = 3) -> void"));
QVERIFY(hasItem(proposal, "f(char c, int optional1 = 3, int optional2 = 3) -> void"));
QVERIFY(hasItem(proposal, "f(const TType<QString> *t) -> void"));
QVERIFY(hasItem(proposal, "f(bool) -> TType<QString>"));
QVERIFY(hasItem(proposal, "f(<b>int a</b>) -&gt; void"));
QVERIFY(hasItem(proposal, "f(<b>const QString &amp;s</b>) -&gt; void"));
QVERIFY(hasItem(proposal, "f(<b>char c</b>, int optional = 3) -&gt; void"));
QVERIFY(hasItem(proposal, "f(<b>char c</b>, int optional1 = 3, int optional2 = 3) -&gt; void"));
QVERIFY(hasItem(proposal, "f(<b>const TType&lt;QString&gt; *t</b>) -&gt; void"));
QVERIFY(hasItem(proposal, "f(<b>bool</b>) -&gt; TType&lt;QString&gt;"));
}
void ClangdTestCompletion::testFunctionHintsFiltered()
@@ -1855,7 +1856,6 @@ void ClangdTestCompletion::testFunctionHintsFiltered()
QVERIFY(proposal);
QCOMPARE(proposal->size(), 2);
QVERIFY(hasItem(proposal, "func(const S &amp;s, <b>int j</b>) -&gt; void"));
QEXPECT_FAIL("", "QTCREATORBUG-26346", Abort);
QVERIFY(hasItem(proposal, "func(const S &amp;s, <b>int j</b>, int k) -&gt; void"));
}
@@ -1868,7 +1868,6 @@ void ClangdTestCompletion::testFunctionHintConstructor()
QVERIFY(!hasItem(proposal, "globalVariable"));
QVERIFY(!hasItem(proposal, " class"));
QVERIFY(hasItem(proposal, "Foo(<b>int</b>)"));
QEXPECT_FAIL("", "QTCREATORBUG-26346", Abort);
QVERIFY(hasItem(proposal, "Foo(<b>int</b>, double)"));
}
@@ -2066,7 +2065,8 @@ void ClangdTestCompletion::getProposal(const QString &fileName,
{
const TextDocument * const doc = document(fileName);
QVERIFY(doc);
const int pos = doc->document()->toPlainText().indexOf(" /* COMPLETE HERE */");
const QString docContent = doc->document()->toPlainText();
const int pos = docContent.indexOf(" /* COMPLETE HERE */");
QVERIFY(pos != -1);
if (cursorPos)
*cursorPos = pos;
@@ -2110,6 +2110,13 @@ void ClangdTestCompletion::getProposal(const QString &fileName,
QVERIFY(timer.isActive());
QVERIFY(proposal);
proposalModel = proposal->model();
if (auto functionHintModel = proposalModel.dynamicCast<IFunctionHintProposalModel>()) {
const int proposalBasePos = proposal->basePosition();
// The language client function hint model expects that activeArgument was called before the
// text of individual hints is accessed. This is usually done by the proposal widget. But
// since we don't have a proposal widget in this test, we have to call it manually.
functionHintModel->activeArgument(docContent.mid(proposalBasePos, pos - proposalBasePos));
}
delete proposal;
// The "dot" test files are only used once.