Commit b54e1356 authored by Milian Wolff's avatar Milian Wolff Committed by Christoph Cullmann
Browse files

WIP: Add support for semantic highlighting in the language server

See https://github.com/microsoft/vscode-languageserver-node/pull/367

This is mostly a POC at this point. It works for the clangd highlight
support found in clang 9.0+ - I haven't tested any other
implementations.

Most notably, this patch will need more work in making the highlight
attributes configurable. Currently I'm just using random colors
that show what's possible, but they result in some serious eye cancer.
To fix this, I believe we finally have to open up more of the
available schema highlighting information and make it accessible
through a new interface or similar in KTextEditor. Then we can either
try to match scope names to KTextEditor attribute names, or better
yet we expand our highlighting files with something like TextMate
scopes to allow direct matching.

Furthermore, this patch also needs more work in its handling of
moving ranges. Currently, it just discards everything and starts
over whenever we get a new notification but that's not really ideal.
Most of the time nothing big will have changed, so we should try
to recycle the previously available ranges instead. Additionally,
we should consider leveraging the ability to map between versions
to translate ranges if the document got updated in-between. At the
minimum, we should discard highlighting requests that don't match
the current version.
parent 06f567fa
......@@ -243,6 +243,7 @@ class LSPClientActionView : public QObject
// applied search ranges
typedef QMultiHash<KTextEditor::Document *, KTextEditor::MovingRange *> RangeCollection;
RangeCollection m_ranges;
QHash<KTextEditor::Document *, QHash<int, QVector<KTextEditor::MovingRange*>>> m_semanticHighlightRanges;
// applied marks
typedef QSet<KTextEditor::Document *> DocumentCollection;
DocumentCollection m_marks;
......@@ -1418,6 +1419,174 @@ public:
updateState();
}
KTextEditor::View *viewForUrl(const QUrl &url) const
{
for (auto *view : m_mainWindow->views()) {
if (view->document()->url() == url)
return view;
}
return nullptr;
}
Q_SLOT void clearSemanticHighlighting(KTextEditor::Document *document)
{
auto &documentRanges = m_semanticHighlightRanges[document];
for (const auto &lineRanges : documentRanges)
qDeleteAll(lineRanges);
documentRanges.clear();
}
void onSemanticHighlighting(const LSPSemanticHighlightingParams &params)
{
auto *view = viewForUrl(params.textDocument.uri);
if (!view) {
qWarning() << "failed to find view for uri" << params.textDocument.uri;
return;
}
auto server = m_serverManager->findServer(view);
if (!server) {
qWarning() << "failed to find server for view" << params.textDocument.uri;
return;
}
auto *document = view->document();
auto *miface = qobject_cast<KTextEditor::MovingInterface *>(document);
Q_ASSERT(miface);
// TODO: translate between locked revision, if possible?
auto version = params.textDocument.version;
if (version == -1) { // use version from disk
version = miface->lastSavedRevision();
if (version == -1) { // never saved
version = miface->revision();
}
}
if (version != miface->revision()) {
qWarning() << "discarding highlighting, versions don't match:"
<< params.textDocument.version << version << miface->revision();
return;
}
// ensure runtime match
connect(document, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document *)), this, SLOT(clearSemanticHighlighting(KTextEditor::Document *)), Qt::UniqueConnection);
connect(document, SIGNAL(aboutToDeleteMovingInterfaceContent(KTextEditor::Document *)), this, SLOT(clearSemanticHighlighting(KTextEditor::Document *)), Qt::UniqueConnection);
// TODO: make schema attributes accessible via some new interface,
// or at least add configuration to the lsp plugin config
auto attributeForScopes = [view](const QVector<QString> &scopes) -> KTextEditor::Attribute::Ptr {
for (const auto &scope : scopes) {
if (scope == QLatin1String("entity.name.function.method.cpp")) {
static KTextEditor::Attribute::Ptr attr;
if (!attr) {
attr = view->defaultStyleAttribute(KTextEditor::dsFunction);
attr.detach();
attr->setForeground(Qt::yellow);
attr->setFontItalic(true);
}
return attr;
} else if(scope == QLatin1String("entity.name.function.cpp")) {
static KTextEditor::Attribute::Ptr attr;
if (!attr) {
attr = view->defaultStyleAttribute(KTextEditor::dsFunction);
attr.detach();
attr->setForeground(Qt::yellow);
}
return attr;
} else if (scope == QLatin1String("variable.other.cpp")) {
static KTextEditor::Attribute::Ptr attr;
if (!attr) {
attr = view->defaultStyleAttribute(KTextEditor::dsVariable);
attr.detach();
attr->setForeground(Qt::cyan);
}
return attr;
} else if (scope == QLatin1String("variable.other.field.cpp")) {
static KTextEditor::Attribute::Ptr attr;
if (!attr) {
attr = view->defaultStyleAttribute(KTextEditor::dsVariable);
attr.detach();
attr->setForeground(Qt::cyan);
attr->setFontItalic(true);
}
return attr;
} else if (scope == QLatin1String("entity.name.type.enum.cpp")) {
static KTextEditor::Attribute::Ptr attr;
if (!attr) {
attr = view->defaultStyleAttribute(KTextEditor::dsConstant);
attr.detach();
attr->setForeground(Qt::magenta);
}
return attr;
} else if (scope == QLatin1String("variable.other.enummember.cpp")) {
static KTextEditor::Attribute::Ptr attr;
if (!attr) {
attr = view->defaultStyleAttribute(KTextEditor::dsConstant);
attr.detach();
attr->setForeground(Qt::darkMagenta);
}
return attr;
} else if (scope == QLatin1String("entity.name.type.class.cpp")
|| scope == QLatin1String("entity.name.type.template.cpp"))
{
static KTextEditor::Attribute::Ptr attr;
if (!attr) {
attr = view->defaultStyleAttribute(KTextEditor::dsDataType);
attr.detach();
attr->setForeground(Qt::green);
}
return attr;
} else if (scope == QLatin1String("entity.name.namespace.cpp")) {
static KTextEditor::Attribute::Ptr attr;
if (!attr) {
attr = view->defaultStyleAttribute(KTextEditor::dsDataType);
attr.detach();
attr->setForeground(Qt::darkGreen);
}
return attr;
}
}
return {};
};
// TODO: we should try to recycle the moving ranges instead of recreating them all the time
const auto scopes = server->capabilities().semanticHighlightingProvider.scopes;
qDebug() << params.textDocument.uri << scopes;
auto &documentRanges = m_semanticHighlightRanges[document];
QSet<int> handledLines;
for (const auto &line : params.lines) {
handledLines.insert(line.line);
auto &lineRanges = documentRanges[line.line];
qDeleteAll(lineRanges);
lineRanges.clear();
qDebug() << "line:" << line.line;
for (const auto &token : line.tokens) {
qDebug() << "token:" << token.character << token.length << token.scope << scopes.value(token.scope);
auto attribute = attributeForScopes(scopes.value(token.scope));
if (!attribute)
continue;
const auto columnStart = static_cast<int>(token.character);
const auto columnEnd = columnStart + static_cast<int>(token.length);
constexpr auto expand = KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight;
auto *range = miface->newMovingRange({line.line, columnStart, line.line, columnEnd},
expand, KTextEditor::MovingRange::InvalidateIfEmpty);
range->setAttribute(attribute);
}
}
// clear lines that got removed or commented out
for (auto it = documentRanges.begin(); it != documentRanges.end();) {
if (!handledLines.contains(it.key())) {
qDeleteAll(it.value());
it = documentRanges.erase(it);
} else {
++it;
}
}
}
void onDocumentUrlChanged(KTextEditor::Document *doc)
{
// url already changed by this time and new url not useful
......@@ -1486,6 +1655,7 @@ public:
renameEnabled = caps.renameProvider;
connect(server.data(), &LSPClientServer::publishDiagnostics, this, &self_type::onDiagnostics, Qt::UniqueConnection);
connect(server.data(), &LSPClientServer::semanticHighlighting, this, &self_type::onSemanticHighlighting, Qt::UniqueConnection);
connect(server.data(), &LSPClientServer::applyEdit, this, &self_type::onApplyEdit, Qt::UniqueConnection);
// update format trigger characters
......
......@@ -75,6 +75,11 @@ struct LSPSignatureHelpOptions {
struct LSPDocumentOnTypeFormattingOptions : public LSPSignatureHelpOptions {
};
struct LSPSemanticHighlightingOptions {
// cf. https://manual.macromates.com/en/language_grammars
QVector<QVector<QString>> scopes;
};
struct LSPServerCapabilities {
LSPDocumentSyncKind textDocumentSync = LSPDocumentSyncKind::None;
bool hoverProvider = false;
......@@ -92,6 +97,7 @@ struct LSPServerCapabilities {
bool renameProvider = false;
// CodeActionOptions not useful/considered at present
bool codeActionProvider = false;
LSPSemanticHighlightingOptions semanticHighlightingProvider;
};
enum class LSPMarkupKind { None = 0, PlainText = 1, MarkDown = 2 };
......@@ -272,6 +278,28 @@ struct LSPPublishDiagnosticsParams {
QList<LSPDiagnostic> diagnostics;
};
struct LSPSemanticHighlightingToken {
quint32 character = 0;
quint16 length = 0;
quint16 scope = 0;
};
Q_DECLARE_TYPEINFO(LSPSemanticHighlightingToken, Q_MOVABLE_TYPE);
struct LSPSemanticHighlightingInformation {
int line = -1;
QVector<LSPSemanticHighlightingToken> tokens;
};
struct LSPVersionedTextDocumentIdentifier {
QUrl uri;
int version = -1;
};
struct LSPSemanticHighlightingParams {
LSPVersionedTextDocumentIdentifier textDocument;
QVector<LSPSemanticHighlightingInformation> lines;
};
struct LSPCommand {
QString title;
QString command;
......
......@@ -36,6 +36,7 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QTime>
#include <QtEndian>
#include <utility>
// good/bad old school; allows easier concatenate
......@@ -270,6 +271,23 @@ static void from_json(LSPDocumentOnTypeFormattingOptions &options, const QJsonVa
}
}
static void from_json(LSPSemanticHighlightingOptions &options, const QJsonValue &json)
{
if (!json.isObject())
return;
const auto scopes = json.toObject().value(QStringLiteral("scopes"));
options.scopes.clear();
for (const auto &scope_entry : scopes.toArray()) {
QVector<QString> entries;
const auto json_entries = scope_entry.toArray();
entries.reserve(json_entries.size());
for (const auto &inner_json_entry : json_entries) {
entries.push_back(inner_json_entry.toString());
}
options.scopes.push_back(entries);
}
}
static void from_json(LSPServerCapabilities &caps, const QJsonObject &json)
{
auto sync = json.value(QStringLiteral("textDocumentSync"));
......@@ -288,6 +306,7 @@ static void from_json(LSPServerCapabilities &caps, const QJsonObject &json)
caps.renameProvider = json.value(QStringLiteral("renameProvider")).toBool();
auto codeActionProvider = json.value(QStringLiteral("codeActionProvider"));
caps.codeActionProvider = codeActionProvider.toBool() || codeActionProvider.isObject();
from_json(caps.semanticHighlightingProvider, json.value(QStringLiteral("semanticHighlighting")).toObject());
}
// follow suit; as performed in kate docmanager
......@@ -654,6 +673,47 @@ static LSPApplyWorkspaceEditParams parseApplyWorkspaceEditParams(const QJsonObje
return ret;
}
static LSPVersionedTextDocumentIdentifier parseVersionedTextDocumentIdentifier(const QJsonObject &result)
{
LSPVersionedTextDocumentIdentifier ret;
ret.uri = normalizeUrl(QUrl(result.value(MEMBER_URI).toString()));
ret.version = result.value(QStringLiteral("version")).toInt(-1);
return ret;
}
static LSPSemanticHighlightingParams parseSemanticHighlighting(const QJsonObject &result)
{
LSPSemanticHighlightingParams ret;
ret.textDocument = parseVersionedTextDocumentIdentifier(result.value(QStringLiteral("textDocument")).toObject());
for (const auto &line_json : result.value(QStringLiteral("lines")).toArray()) {
const auto line_obj = line_json.toObject();
LSPSemanticHighlightingInformation info;
info.line = line_obj.value(QStringLiteral("line")).toInt(-1);
const auto tokenString = line_obj.value(QStringLiteral("tokens"));
constexpr auto TokenSize = sizeof(LSPSemanticHighlightingToken);
// the raw tokens are in big endian, we may need to convert that to little endian
const auto rawTokens = QByteArray::fromBase64(tokenString.toString().toUtf8());
if (rawTokens.size() % TokenSize != 0) {
qWarning() << "unexpected raw token size" << rawTokens.size() << "for string" << tokenString << "in line" << info.line;
continue;
}
const auto numTokens = rawTokens.size() / TokenSize;
const auto *begin = reinterpret_cast<const LSPSemanticHighlightingToken *>(rawTokens.constData());
const auto *end = begin + numTokens;
info.tokens.resize(numTokens);
std::transform(begin, end, info.tokens.begin(), [](const LSPSemanticHighlightingToken &rawToken) {
LSPSemanticHighlightingToken token;
token.character = qFromBigEndian(rawToken.character);
token.length = qFromBigEndian(rawToken.length);
token.scope = qFromBigEndian(rawToken.scope);
return token;
});
ret.lines.push_back(info);
}
return ret;
}
using GenericReplyType = QJsonValue;
using GenericReplyHandler = ReplyHandler<GenericReplyType>;
......@@ -923,7 +983,8 @@ private:
QJsonObject {{QStringLiteral("hierarchicalDocumentSymbolSupport"), true}},
},
{QStringLiteral("publishDiagnostics"), QJsonObject {{QStringLiteral("relatedInformation"), true}}},
{QStringLiteral("codeAction"), codeAction}}}};
{QStringLiteral("codeAction"), codeAction},
{QStringLiteral("semanticHighlightingCapabilities"), QJsonObject {{QStringLiteral("semanticHighlighting"), true}}}}}};
// NOTE a typical server does not use root all that much,
// other than for some corner case (in) requests
QJsonObject params {{QStringLiteral("processId"), QCoreApplication::applicationPid()},
......@@ -1097,6 +1158,8 @@ public:
auto method = msg[MEMBER_METHOD].toString();
if (method == QLatin1String("textDocument/publishDiagnostics")) {
emit q->publishDiagnostics(parseDiagnostics(msg[MEMBER_PARAMS].toObject()));
} else if (method == QLatin1String("textDocument/semanticHighlighting")) {
emit q->semanticHighlighting(parseSemanticHighlighting(msg[MEMBER_PARAMS].toObject()));
} else {
qCWarning(LSPCLIENT) << "discarding notification" << method;
}
......
......@@ -142,6 +142,7 @@ public:
// notification = signal
Q_SIGNALS:
void publishDiagnostics(const LSPPublishDiagnosticsParams &);
void semanticHighlighting(const LSPSemanticHighlightingParams &);
// request = signal
void applyEdit(const LSPApplyWorkspaceEditParams &req, const ApplyEditReplyHandler &h, bool &handled);
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment