qmlls: Add autofix capabilities

Allows qmlls to make use of qmllint's existing autofix infrastructure
and translates them into code actions.

Change-Id: I855e131f621b16a8bb72997aa8e2b36833ac4dab
Reviewed-by: Fawzi Mohamed <fawzi.mohamed@qt.io>
This commit is contained in:
Maximilian Goldstein 2022-03-22 11:53:45 +01:00
parent a7c5f86419
commit 5fc74d216f
4 changed files with 168 additions and 1 deletions

View File

@ -73,6 +73,17 @@ public:
return num;
}
QList<Diagnostic> diagnostics(const QByteArray &uri) const
{
QList<Diagnostic> result;
for (const auto &params : m_received) {
if (params.uri == uri)
result << params.diagnostics;
}
return result;
}
void clear() { m_received.clear(); }
private:
@ -159,6 +170,75 @@ void tst_Qmlls::didOpenTextDocument()
QTRY_VERIFY_WITH_TIMEOUT(m_diagnosticsHandler.numDiagnostics(uri) != 0, 10000);
QVERIFY(m_diagnosticsHandler.contains(uri, 3, 4, 3, 10));
auto diagnostics = m_diagnosticsHandler.diagnostics(uri);
CodeActionParams codeActionParams;
codeActionParams.textDocument = { textDocument.uri };
codeActionParams.context.diagnostics = diagnostics;
codeActionParams.range.start = Position { 0, 0 };
codeActionParams.range.end =
Position { static_cast<int>(textDocument.text.split(u'\n').size()), 0 };
bool success = false;
m_protocol.requestCodeAction(
codeActionParams,
[&](const std::variant<QList<std::variant<Command, CodeAction>>, std::nullptr_t>
&response) {
using ListType = QList<std::variant<Command, CodeAction>>;
QVERIFY(std::holds_alternative<ListType>(response));
auto list = std::get<ListType>(response);
QList<QPair<QString, QString>> expectedData = {
{ QLatin1StringView("Did you mean \"width\"?"), QLatin1StringView("width") },
{ QLatin1StringView("Did you mean \"z\"?"), QLatin1StringView("z") }
};
QCOMPARE(list.size(), expectedData.size());
for (const auto &entry : list) {
QVERIFY(std::holds_alternative<CodeAction>(entry));
CodeAction action = std::get<CodeAction>(entry);
QString title = QString::fromUtf8(action.title);
QVERIFY(action.kind.has_value());
QCOMPARE(QString::fromUtf8(action.kind.value()),
QLatin1StringView("refactor.rewrite"));
QVERIFY(action.edit.has_value());
WorkspaceEdit edit = action.edit.value();
QVERIFY(edit.documentChanges.has_value());
auto docChangeVariant = edit.documentChanges.value();
QVERIFY(std::holds_alternative<QList<TextDocumentEdit>>(docChangeVariant));
auto documentChanges = std::get<QList<TextDocumentEdit>>(docChangeVariant);
QCOMPARE(documentChanges.size(), 1);
TextDocumentEdit textDocEdit = documentChanges.first();
QCOMPARE(textDocEdit.textDocument.uri, textDocument.uri);
QCOMPARE(textDocEdit.edits.size(), 1);
auto editVariant = textDocEdit.edits.first();
QVERIFY(std::holds_alternative<TextEdit>(editVariant));
TextEdit textEdit = std::get<TextEdit>(editVariant);
QString newText = QString::fromUtf8(textEdit.newText);
QPair<QString, QString> data = { title, newText };
qsizetype dataIndex = expectedData.indexOf(data);
QVERIFY2(dataIndex != -1,
qPrintable(QLatin1String("{\"%1\",\"%2\"}").arg(title, newText)));
// Make sure every expected entry only occurs once
expectedData.remove(dataIndex);
}
success = true;
},
[](const QLspSpecification::ResponseError &error) {
qWarning() << "CodeAction Error:" << QString::fromUtf8(error.message);
});
QTRY_VERIFY_WITH_TIMEOUT(success, 10000);
m_diagnosticsHandler.clear();
}

View File

@ -73,6 +73,64 @@ static DiagnosticSeverity severityFromMsgType(QtMsgType t)
return DiagnosticSeverity::Error;
}
static void codeActionHandler(
const QByteArray &, const CodeActionParams &params,
LSPPartialResponse<std::variant<QList<std::variant<Command, CodeAction>>, std::nullptr_t>,
QList<std::variant<Command, CodeAction>>> &&response)
{
QList<std::variant<Command, CodeAction>> responseData;
for (const Diagnostic &diagnostic : params.context.diagnostics) {
if (!diagnostic.data.has_value())
continue;
QJsonArray suggestions = diagnostic.data.value().toArray();
QList<TextDocumentEdit> edits;
QString message;
for (const QJsonValue &suggestion : suggestions) {
QString replacement = suggestion[u"replacement"].toString();
message += suggestion[u"message"].toString() + u"\n";
TextEdit textEdit;
textEdit.range = { Position { suggestion[u"lspBeginLine"].toInt(),
suggestion[u"lspBeginCharacter"].toInt() },
Position { suggestion[u"lspEndLine"].toInt(),
suggestion[u"lspEndCharacter"].toInt() } };
textEdit.newText = replacement.toUtf8();
TextDocumentEdit textDocEdit;
textDocEdit.textDocument = { params.textDocument };
textDocEdit.edits.append(textEdit);
edits.append(textDocEdit);
}
message.chop(1);
WorkspaceEdit edit;
edit.documentChanges = edits;
CodeAction action;
action.kind = u"refactor.rewrite"_qs.toUtf8();
action.edit = edit;
action.title = message.toUtf8();
responseData.append(action);
}
response.sendResponse(responseData);
}
void QmlLintSuggestions::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol)
{
protocol->registerCodeActionRequestHandler(&codeActionHandler);
}
void QmlLintSuggestions::setupCapabilities(const QLspSpecification::InitializeParams &,
QLspSpecification::InitializeResult &serverInfo)
{
serverInfo.capabilities.codeActionProvider = true;
}
QmlLintSuggestions::QmlLintSuggestions(QLanguageServer *server, QmlLsp::QQmlCodeModel *codeModel)
: m_server(server), m_codeModel(codeModel)
{
@ -168,6 +226,29 @@ void QmlLintSuggestions::diagnose(const QByteArray &uri)
addLength(range.end, message[u"charOffset"].toInt(), message[u"length"].toInt());
diagnostic.message = message[u"message"].toString().toUtf8();
diagnostic.source = QByteArray("qmllint");
auto suggestions = message[u"suggestions"].toArray();
if (!suggestions.isEmpty()) {
// We need to interject the information about where the fix suggestions end
// here since we don't have access to the textDocument to calculate it later.
QJsonArray fixedSuggestions;
for (const QJsonValue &value : suggestions) {
QJsonObject object = value.toObject();
int line = message[u"line"].toInt(1) - 1;
int column = message[u"column"].toInt(1) - 1;
object[u"lspBeginLine"] = line;
object[u"lspBeginCharacter"] = column;
Position end = { line, column };
addLength(end, object[u"charOffset"].toInt(), object[u"length"].toInt());
object[u"lspEndLine"] = end.line;
object[u"lspEndCharacter"] = end.character;
fixedSuggestions << object;
}
diagnostic.data = fixedSuggestions;
}
return diagnostic;
};
doc.iterateErrors(

View File

@ -41,13 +41,18 @@ struct LastLintUpdate
std::optional<QDateTime> invalidUpdatesSince;
};
class QmlLintSuggestions : public QObject
class QmlLintSuggestions : public QLanguageServerModule
{
Q_OBJECT
public:
QmlLintSuggestions(QLanguageServer *server, QmlLsp::QQmlCodeModel *codeModel);
QString name() const override { return QLatin1StringView("QmlLint Suggestions"); }
public slots:
void diagnose(const QByteArray &uri);
void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) override;
void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo,
QLspSpecification::InitializeResult &) override;
private:
QMutex m_mutex;

View File

@ -69,6 +69,7 @@ QQmlLanguageServer::QQmlLanguageServer(std::function<void(const QByteArray &)> s
{
m_server.addServerModule(this);
m_server.addServerModule(&m_textSynchronization);
m_server.addServerModule(&m_lint);
m_server.finishSetup();
qCWarning(lspServerLog) << "Did Setup";
}