diff options
| author | David Schulz <david.schulz@qt.io> | 2021-02-25 12:54:14 +0100 |
|---|---|---|
| committer | David Schulz <david.schulz@qt.io> | 2021-04-21 11:33:56 +0000 |
| commit | f1bb3b6811ab70e54b5e494ef2ed40dc7eec4c96 (patch) | |
| tree | 30405ecb68de24386ec75cc6b82b0bc1f71c4680 | |
| parent | 9c2980f65d0230fb08f27256c368bcc17f2e92a0 (diff) | |
| download | qt-creator-f1bb3b6811ab70e54b5e494ef2ed40dc7eec4c96.tar.gz | |
LSP: add semantic tokens
Change-Id: Ia6865ec6991ec62ae9f0dc2dfa692f1f27318ed1
Reviewed-by: Christian Stenger <christian.stenger@qt.io>
| -rw-r--r-- | src/libs/languageserverprotocol/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | src/libs/languageserverprotocol/clientcapabilities.cpp | 54 | ||||
| -rw-r--r-- | src/libs/languageserverprotocol/clientcapabilities.h | 83 | ||||
| -rw-r--r-- | src/libs/languageserverprotocol/jsonkeys.h | 14 | ||||
| -rw-r--r-- | src/libs/languageserverprotocol/languageserverprotocol.pro | 2 | ||||
| -rw-r--r-- | src/libs/languageserverprotocol/languageserverprotocol.qbs | 2 | ||||
| -rw-r--r-- | src/libs/languageserverprotocol/semantictokens.cpp | 154 | ||||
| -rw-r--r-- | src/libs/languageserverprotocol/semantictokens.h | 241 | ||||
| -rw-r--r-- | src/libs/languageserverprotocol/servercapabilities.cpp | 63 | ||||
| -rw-r--r-- | src/libs/languageserverprotocol/servercapabilities.h | 48 | ||||
| -rw-r--r-- | src/plugins/languageclient/client.cpp | 47 | ||||
| -rw-r--r-- | src/plugins/languageclient/client.h | 6 | ||||
| -rw-r--r-- | src/plugins/languageclient/languageclientmanager.cpp | 10 | ||||
| -rw-r--r-- | src/plugins/languageclient/semantichighlightsupport.cpp | 339 | ||||
| -rw-r--r-- | src/plugins/languageclient/semantichighlightsupport.h | 46 |
15 files changed, 1073 insertions, 37 deletions
diff --git a/src/libs/languageserverprotocol/CMakeLists.txt b/src/libs/languageserverprotocol/CMakeLists.txt index dec86105ad..55415e2db7 100644 --- a/src/libs/languageserverprotocol/CMakeLists.txt +++ b/src/libs/languageserverprotocol/CMakeLists.txt @@ -17,6 +17,7 @@ add_qtc_library(LanguageServerProtocol lsputils.cpp lsputils.h messages.cpp messages.h progresssupport.cpp progresssupport.h + semantictokens.cpp semantictokens.h servercapabilities.cpp servercapabilities.h shutdownmessages.cpp shutdownmessages.h textsynchronization.cpp textsynchronization.h diff --git a/src/libs/languageserverprotocol/clientcapabilities.cpp b/src/libs/languageserverprotocol/clientcapabilities.cpp index c174f54c9c..c3665462f8 100644 --- a/src/libs/languageserverprotocol/clientcapabilities.cpp +++ b/src/libs/languageserverprotocol/clientcapabilities.cpp @@ -47,4 +47,58 @@ WorkspaceClientCapabilities::WorkspaceClientCapabilities() setWorkspaceFolders(true); } +Utils::optional<Utils::variant<bool, QJsonObject>> SemanticTokensClientCapabilities::Requests::range() + const +{ + using RetType = Utils::variant<bool, QJsonObject>; + const QJsonValue &rangeOptions = value(rangeKey); + if (rangeOptions.isBool()) + return RetType(rangeOptions.toBool()); + if (rangeOptions.isObject()) + return RetType(rangeOptions.toObject()); + return Utils::nullopt; +} + +void SemanticTokensClientCapabilities::Requests::setRange( + const Utils::variant<bool, QJsonObject> &range) +{ + insertVariant<bool, QJsonObject>(rangeKey, range); +} + +Utils::optional<Utils::variant<bool, FullSemanticTokenOptions>> +SemanticTokensClientCapabilities::Requests::full() const +{ + using RetType = Utils::variant<bool, FullSemanticTokenOptions>; + const QJsonValue &fullOptions = value(fullKey); + if (fullOptions.isBool()) + return RetType(fullOptions.toBool()); + if (fullOptions.isObject()) + return RetType(FullSemanticTokenOptions(fullOptions.toObject())); + return Utils::nullopt; +} + +void SemanticTokensClientCapabilities::Requests::setFull( + const Utils::variant<bool, FullSemanticTokenOptions> &full) +{ + insertVariant<bool, FullSemanticTokenOptions>(fullKey, full); +} + +Utils::optional<SemanticTokensClientCapabilities> TextDocumentClientCapabilities::semanticTokens() + const +{ + return optionalValue<SemanticTokensClientCapabilities>(semanticTokensKey); +} + +void TextDocumentClientCapabilities::setSemanticTokens( + const SemanticTokensClientCapabilities &semanticTokens) +{ + insert(semanticTokensKey, semanticTokens); +} + +bool SemanticTokensClientCapabilities::isValid() const +{ + return contains(requestsKey) && contains(tokenTypesKey) && contains(tokenModifiersKey) + && contains(formatsKey); +} + } // namespace LanguageServerProtocol diff --git a/src/libs/languageserverprotocol/clientcapabilities.h b/src/libs/languageserverprotocol/clientcapabilities.h index d8f5d93533..f2b12fea68 100644 --- a/src/libs/languageserverprotocol/clientcapabilities.h +++ b/src/libs/languageserverprotocol/clientcapabilities.h @@ -27,6 +27,7 @@ #include "jsonkeys.h" #include "lsptypes.h" +#include "semantictokens.h" namespace LanguageServerProtocol { @@ -40,6 +41,84 @@ public: void clearDynamicRegistration() { remove(dynamicRegistrationKey); } }; +class LANGUAGESERVERPROTOCOL_EXPORT FullSemanticTokenOptions : public JsonObject +{ +public: + using JsonObject::JsonObject; + + /** + * The client will send the `textDocument/semanticTokens/full/delta` + * request if the server provides a corresponding handler. + */ + Utils::optional<bool> delta() const { return optionalValue<bool>(deltaKey); } + void setDelta(bool delta) { insert(deltaKey, delta); } + void clearDelta() { remove(deltaKey); } +}; + +class LANGUAGESERVERPROTOCOL_EXPORT SemanticTokensClientCapabilities : public DynamicRegistrationCapabilities +{ +public: + using DynamicRegistrationCapabilities::DynamicRegistrationCapabilities; + class LANGUAGESERVERPROTOCOL_EXPORT Requests : public JsonObject + { + /** + * Which requests the client supports and might send to the server + * depending on the server's capability. Please note that clients might not + * show semantic tokens or degrade some of the user experience if a range + * or full request is advertised by the client but not provided by the + * server. If for example the client capability `requests.full` and + * `request.range` are both set to true but the server only provides a + * range provider the client might not render a minimap correctly or might + * even decide to not show any semantic tokens at all. + */ + public: + using JsonObject::JsonObject; + + /** + * The client will send the `textDocument/semanticTokens/range` request + * if the server provides a corresponding handler. + */ + Utils::optional<Utils::variant<bool, QJsonObject>> range() const; + void setRange(const Utils::variant<bool, QJsonObject> &range); + void clearRange() { remove(rangeKey); } + + /** + * The client will send the `textDocument/semanticTokens/full` request + * if the server provides a corresponding handler. + */ + Utils::optional<Utils::variant<bool, FullSemanticTokenOptions>> full() const; + void setFull(const Utils::variant<bool, FullSemanticTokenOptions> &full); + void clearFull() { remove(fullKey); } + }; + + Requests requests() const { return typedValue<Requests>(requestsKey); } + void setRequests(const Requests &requests) { insert(requestsKey, requests); } + + /// The token types that the client supports. + QList<QString> tokenTypes() const { return array<QString>(tokenTypesKey); } + void setTokenTypes(const QList<QString> &value) { insertArray(tokenTypesKey, value); } + + /// The token modifiers that the client supports. + QList<QString> tokenModifiers() const { return array<QString>(tokenModifiersKey); } + void setTokenModifiers(const QList<QString> &value) { insertArray(tokenModifiersKey, value); } + + /// The formats the clients supports. + QList<QString> formats() const { return array<QString>(formatsKey); } + void setFormats(const QList<QString> &value) { insertArray(formatsKey, value); } + + /// Whether the client supports tokens that can overlap each other. + Utils::optional<bool> overlappingTokenSupport() const { return optionalValue<bool>(overlappingTokenSupportKey); } + void setOverlappingTokenSupport(bool overlappingTokenSupport) { insert(overlappingTokenSupportKey, overlappingTokenSupport); } + void clearOverlappingTokenSupport() { remove(overlappingTokenSupportKey); } + + /// Whether the client supports tokens that can span multiple lines. + Utils::optional<bool> multiLineTokenSupport() const { return optionalValue<bool>(multiLineTokenSupportKey); } + void setMultiLineTokenSupport(bool multiLineTokenSupport) { insert(multiLineTokenSupportKey, multiLineTokenSupport); } + void clearMultiLineTokenSupport() { remove(multiLineTokenSupportKey); } + + bool isValid() const override; +}; + class LANGUAGESERVERPROTOCOL_EXPORT SymbolCapabilities : public DynamicRegistrationCapabilities { public: @@ -432,6 +511,10 @@ public: void setRename(const RenameClientCapabilities &rename) { insert(renameKey, rename); } void clearRename() { remove(renameKey); } + + Utils::optional<SemanticTokensClientCapabilities> semanticTokens() const; + void setSemanticTokens(const SemanticTokensClientCapabilities &semanticTokens); + void clearSemanticTokens() { remove(semanticTokensKey); } }; class LANGUAGESERVERPROTOCOL_EXPORT WorkspaceClientCapabilities : public JsonObject diff --git a/src/libs/languageserverprotocol/jsonkeys.h b/src/libs/languageserverprotocol/jsonkeys.h index 227f17f84d..029a4a8b28 100644 --- a/src/libs/languageserverprotocol/jsonkeys.h +++ b/src/libs/languageserverprotocol/jsonkeys.h @@ -79,6 +79,8 @@ constexpr char dataKey[] = "data"; constexpr char defaultCharset[] = "utf-8"; constexpr char definitionKey[] = "definition"; constexpr char definitionProviderKey[] = "definitionProvider"; +constexpr char deleteCountKey[] = "deleteCount"; +constexpr char deltaKey[] = "delta"; constexpr char deprecatedKey[] = "deprecated"; constexpr char detailKey[] = "detail"; constexpr char diagnosticsKey[] = "diagnostics"; @@ -108,7 +110,9 @@ constexpr char executeCommandProviderKey[] = "executeCommandProvider"; constexpr char experimentalKey[] = "experimental"; constexpr char filterTextKey[] = "filterText"; constexpr char firstTriggerCharacterKey[] = "firstTriggerCharacter"; +constexpr char formatsKey[] = "formats"; constexpr char formattingKey[] = "formatting"; +constexpr char fullKey[] = "full"; constexpr char greenKey[] = "green"; constexpr char headerFieldSeparator[] = ": "; constexpr char headerSeparator[] = "\r\n"; @@ -131,12 +135,14 @@ constexpr char kindKey[] = "kind"; constexpr char labelKey[] = "label"; constexpr char languageIdKey[] = "languageId"; constexpr char languageKey[] = "language"; +constexpr char legendKey[] = "legend"; constexpr char lineKey[] = "line"; constexpr char linesKey[] = "lines"; constexpr char locationKey[] = "location"; constexpr char messageKey[] = "message"; constexpr char methodKey[] = "method"; constexpr char moreTriggerCharacterKey[] = "moreTriggerCharacter"; +constexpr char multiLineTokenSupportKey[] = "multiLineTokenSupport"; constexpr char nameKey[] = "name"; constexpr char newNameKey[] = "newName"; constexpr char newTextKey[] = "newText"; @@ -144,6 +150,7 @@ constexpr char onTypeFormattingKey[] = "onTypeFormatting"; constexpr char onlyKey[] = "only"; constexpr char openCloseKey[] = "openClose"; constexpr char optionsKey[] = "options"; +constexpr char overlappingTokenSupportKey[] = "overlappingTokenSupport"; constexpr char parametersKey[] = "params"; constexpr char patternKey[] = "pattern"; constexpr char percentageKey[] = "percentage"; @@ -151,6 +158,7 @@ constexpr char placeHolderKey[] = "placeHolder"; constexpr char positionKey[] = "position"; constexpr char prepareProviderKey[] = "prepareProvider"; constexpr char prepareSupportKey[] = "prepareSupport"; +constexpr char previousResultIdKey[] = "previousResultId"; constexpr char processIdKey[] = "processId"; constexpr char queryKey[] = "query"; constexpr char rangeFormattingKey[] = "rangeFormatting"; @@ -165,7 +173,9 @@ constexpr char registrationsKey[] = "registrations"; constexpr char removedKey[] = "removed"; constexpr char renameKey[] = "rename"; constexpr char renameProviderKey[] = "renameProvider"; +constexpr char requestsKey[] = "requests"; constexpr char resolveProviderKey[] = "resolveProvider"; +constexpr char resultIdKey[] = "resultId"; constexpr char resultKey[] = "result"; constexpr char retryKey[] = "retry"; constexpr char rootPathKey[] = "rootPath"; @@ -178,6 +188,8 @@ constexpr char sectionKey[] = "section"; constexpr char selectionRangeKey[] = "selectionRange"; constexpr char semanticHighlightingCapabilitiesKey[] = "semanticHighlightingCapabilities"; constexpr char semanticHighlightingKey[] = "semanticHighlighting"; +constexpr char semanticTokensKey[] = "semanticTokens"; +constexpr char semanticTokensProviderKey[] = "semanticTokensProvider"; constexpr char settingsKey[] = "settings"; constexpr char severityKey[] = "severity"; constexpr char signatureHelpKey[] = "signatureHelp"; @@ -201,6 +213,8 @@ constexpr char textEditKey[] = "textEdit"; constexpr char textKey[] = "text"; constexpr char titleKey[] = "title"; constexpr char tokenKey[] = "token"; +constexpr char tokenModifiersKey[] = "tokenModifiers"; +constexpr char tokenTypesKey[] = "tokenTypes"; constexpr char tokensKey[] = "tokens"; constexpr char traceKey[] = "trace"; constexpr char triggerCharacterKey[] = "triggerCharacter"; diff --git a/src/libs/languageserverprotocol/languageserverprotocol.pro b/src/libs/languageserverprotocol/languageserverprotocol.pro index befa93efa6..33651a9280 100644 --- a/src/libs/languageserverprotocol/languageserverprotocol.pro +++ b/src/libs/languageserverprotocol/languageserverprotocol.pro @@ -18,6 +18,7 @@ HEADERS += \ lsputils.h \ messages.h \ progresssupport.h \ + semantictokens.h \ servercapabilities.h \ shutdownmessages.h \ textsynchronization.h \ @@ -37,6 +38,7 @@ SOURCES += \ lsputils.cpp \ messages.cpp \ progresssupport.cpp \ + semantictokens.cpp \ servercapabilities.cpp \ shutdownmessages.cpp \ textsynchronization.cpp \ diff --git a/src/libs/languageserverprotocol/languageserverprotocol.qbs b/src/libs/languageserverprotocol/languageserverprotocol.qbs index 4b6df40ea2..97e5b5d1c7 100644 --- a/src/libs/languageserverprotocol/languageserverprotocol.qbs +++ b/src/libs/languageserverprotocol/languageserverprotocol.qbs @@ -37,6 +37,8 @@ Project { "messages.h", "progresssupport.cpp", "progresssupport.h", + "semantictokens.cpp", + "semantictokens.h", "servercapabilities.cpp", "servercapabilities.h", "shutdownmessages.cpp", diff --git a/src/libs/languageserverprotocol/semantictokens.cpp b/src/libs/languageserverprotocol/semantictokens.cpp new file mode 100644 index 0000000000..4274dbfc12 --- /dev/null +++ b/src/libs/languageserverprotocol/semantictokens.cpp @@ -0,0 +1,154 @@ +/**************************************************************************** +** +** Copyright (C) 2021 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 "semantictokens.h" + +namespace LanguageServerProtocol { + +bool SemanticTokensLegend::isValid() const +{ + return contains(tokenTypesKey) && contains(tokenModifiersKey); +} + +QMap<QString, int> SemanticTokens::defaultTokenTypesMap() +{ + QMap<QString, int> map; + map.insert("namespace", namespaceToken); + map.insert("type", typeToken); + map.insert("class", classToken); + map.insert("enum", enumToken); + map.insert("interface", interfaceToken); + map.insert("struct", structToken); + map.insert("typeParameter", typeParameterToken); + map.insert("parameter", parameterToken); + map.insert("variable", variableToken); + map.insert("property", propertyToken); + map.insert("enumMember", enumMemberToken); + map.insert("event", eventToken); + map.insert("function", functionToken); + map.insert("method", methodToken); + map.insert("macro", macroToken); + map.insert("keyword", keywordToken); + map.insert("modifier", modifierToken); + map.insert("comment", commentToken); + map.insert("string", stringToken); + map.insert("number", numberToken); + map.insert("regexp", regexpToken); + map.insert("operator", operatorToken); + return map; +} + +QMap<QString, int> SemanticTokens::defaultTokenModifiersMap() +{ + QMap<QString, int> map; + map.insert("declaration", declarationModifier); + map.insert("definition", definitionModifier); + map.insert("readonly", readonlyModifier); + map.insert("static", staticModifier); + map.insert("deprecated", deprecatedModifier); + map.insert("abstract", abstractModifier); + map.insert("async", asyncModifier); + map.insert("modification", modificationModifier); + map.insert("documentation", documentationModifier); + map.insert("defaultLibrary", defaultLibraryModifier); + return map; +} + +static int convertModifiers(int modifiersData, const QList<int> &tokenModifiers) +{ + int result = 0; + for (int i = 0; i < tokenModifiers.size() && modifiersData > 0; ++i) { + if (modifiersData & 0x1) + result |= tokenModifiers[i]; + modifiersData = modifiersData >> 1; + } + return result; +} + +QList<SemanticToken> SemanticTokens::toTokens(const QList<int> &tokenTypes, + const QList<int> &tokenModifiers) const +{ + const QList<int> &data = this->data(); + if (data.size() % 5 != 0) + return {}; + QList<SemanticToken> tokens; + tokens.reserve(int(data.size() / 5)); + auto end = data.end(); + for (auto it = data.begin(); it != end; it += 5) { + SemanticToken token; + token.deltaLine = *(it); + token.deltaStart = *(it + 1); + token.length = *(it + 2); + token.tokenType = tokenTypes.value(*(it + 3), -1); + token.tokenModifiers = convertModifiers(*(it + 4), tokenModifiers); + tokens << token; + } + return tokens; +} + +bool SemanticTokensRangeParams::isValid() const +{ + return SemanticTokensParams::isValid() && contains(rangeKey); +} + +SemanticTokensFullRequest::SemanticTokensFullRequest(const SemanticTokensParams ¶ms) + : Request(methodName, params) +{} + +SemanticTokensRangeRequest::SemanticTokensRangeRequest(const SemanticTokensRangeParams ¶ms) + : Request(methodName, params) +{} + +SemanticTokensResult::SemanticTokensResult(const QJsonValue &value) +{ + if (value.isObject()) + emplace<SemanticTokens>(SemanticTokens(value.toObject())); + else + emplace<nullptr_t>(nullptr); +} + +SemanticTokensFullDeltaRequest::SemanticTokensFullDeltaRequest(const SemanticTokensDeltaParams ¶ms) + : Request(methodName, params) +{} + +bool SemanticTokensDeltaParams::isValid() const +{ + return SemanticTokensParams::isValid() && contains(previousResultIdKey); +} + +SemanticTokensDeltaResult::SemanticTokensDeltaResult(const QJsonValue &value) +{ + if (value.isObject()) { + QJsonObject object = value.toObject(); + if (object.contains(editsKey)) + emplace<SemanticTokensDelta>(object); + else + emplace<SemanticTokens>(object); + } else { + emplace<nullptr_t>(nullptr); + } +} + +} // namespace LanguageServerProtocol diff --git a/src/libs/languageserverprotocol/semantictokens.h b/src/libs/languageserverprotocol/semantictokens.h new file mode 100644 index 0000000000..d35704d1cf --- /dev/null +++ b/src/libs/languageserverprotocol/semantictokens.h @@ -0,0 +1,241 @@ +/**************************************************************************** +** +** Copyright (C) 2021 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. +** +****************************************************************************/ + +#pragma once + +#include "jsonkeys.h" +#include "jsonobject.h" +#include "jsonrpcmessages.h" +#include "languageserverprotocol_global.h" +#include "lsptypes.h" + +namespace LanguageServerProtocol { + +struct LANGUAGESERVERPROTOCOL_EXPORT SemanticToken +{ + int deltaLine = 0; + int deltaStart = 0; + int length = 0; + int tokenType = 0; + int tokenModifiers = 0; +}; + +class LANGUAGESERVERPROTOCOL_EXPORT SemanticTokensLegend : public JsonObject +{ +public: + using JsonObject::JsonObject; + + // The token types a server uses. + QList<QString> tokenTypes() const { return array<QString>(tokenTypesKey); } + void setTokenTypes(const QList<QString> &tokenTypes) { insertArray(tokenTypesKey, tokenTypes); } + + // The token modifiers a server uses. + QList<QString> tokenModifiers() const { return array<QString>(tokenModifiersKey); } + void setTokenModifiers(const QList<QString> &value) { insertArray(tokenModifiersKey, value); } + + bool isValid() const override; +}; + +enum SemanticTokenTypes { + namespaceToken, + typeToken, + classToken, + enumToken, + interfaceToken, + structToken, + typeParameterToken, + parameterToken, + variableToken, + propertyToken, + enumMemberToken, + eventToken, + functionToken, + methodToken, + macroToken, + keywordToken, + modifierToken, + commentToken, + stringToken, + numberToken, + regexpToken, + operatorToken +}; + +enum SemanticTokenModifiers { + declarationModifier = 0x1, + definitionModifier = 0x2, + readonlyModifier = 0x4, + staticModifier = 0x8, + deprecatedModifier = 0x10, + abstractModifier = 0x20, + asyncModifier = 0x40, + modificationModifier = 0x80, + documentationModifier = 0x100, + defaultLibraryModifier = 0x200 +}; + +class LANGUAGESERVERPROTOCOL_EXPORT SemanticTokensParams : public JsonObject +{ +public: + using JsonObject::JsonObject; + + TextDocumentIdentifier textDocument() const + { return typedValue<TextDocumentIdentifier>(textDocumentKey); } + void setTextDocument(const TextDocumentIdentifier &textDocument) + { insert(textDocumentKey, textDocument); } + + bool isValid() const override { return contains(textDocumentKey); } +}; + +class LANGUAGESERVERPROTOCOL_EXPORT SemanticTokensDeltaParams : public SemanticTokensParams +{ +public: + using SemanticTokensParams::SemanticTokensParams; + + QString previousResultId() const { return typedValue<QString>(previousResultIdKey); } + void setPreviousResultId(const QString &previousResultId) + { + insert(previousResultIdKey, previousResultId); + } + + bool isValid() const override; +}; + +class LANGUAGESERVERPROTOCOL_EXPORT SemanticTokensRangeParams : public SemanticTokensParams +{ +public: + using SemanticTokensParams::SemanticTokensParams; + + Range range() const { return typedValue<Range>(rangeKey); } + void setRange(const Range &range) { insert(rangeKey, range); } + + bool isValid() const override; +}; + +class LANGUAGESERVERPROTOCOL_EXPORT SemanticTokens : public JsonObject +{ +public: + using JsonObject::JsonObject; + + /** + * An optional result id. If provided and clients support delta updating + * the client will include the result id in the next semantic token request. + * A server can then instead of computing all semantic tokens again simply + * send a delta. + */ + Utils::optional<QString> resultId() const { return optionalValue<QString>(resultIdKey); } + void setResultId(const QString &resultId) { insert(resultIdKey, resultId); } + void clearResultId() { remove(resultIdKey); } + + /// The actual tokens. + QList<int> data() const { return array<int>(dataKey); } + void setData(const QList<int> &value) { insertArray(dataKey, value); } + + bool isValid() const override { return contains(dataKey); } + + QList<SemanticToken> toTokens(const QList<int> &tokenTypes, + const QList<int> &tokenModifiers) const; + static QMap<QString, int> defaultTokenTypesMap(); + static QMap<QString, int> defaultTokenModifiersMap(); +}; + +class LANGUAGESERVERPROTOCOL_EXPORT SemanticTokensResult + : public Utils::variant<SemanticTokens, nullptr_t> +{ +public: + using variant::variant; + explicit SemanticTokensResult(const QJsonValue &value); +}; + +class LANGUAGESERVERPROTOCOL_EXPORT SemanticTokensFullRequest + : public Request<SemanticTokensResult, nullptr_t, SemanticTokensParams> +{ +public: + explicit SemanticTokensFullRequest(const SemanticTokensParams ¶ms); + using Request::Request; + constexpr static const char methodName[] = "textDocument/semanticTokens/full"; +}; + +class SemanticTokensEdit : public JsonObject +{ +public: + using JsonObject::JsonObject; + + int start() const { return typedValue<int>(startKey); } + void setStart(int start) { insert(startKey, start); } + + int deleteCount() const { return typedValue<int>(deleteCountKey); } + void setDeleteCount(int deleteCount) { insert(deleteCountKey, deleteCount); } + + Utils::optional<QList<int>> data() const { return optionalArray<int>(dataKey); } + void setData(const QList<int> &value) { insertArray(dataKey, value); } + void clearData() { remove(dataKey); } + + int dataSize() const { return contains(dataKey) ? value(dataKey).toArray().size() : 0; } + + bool isValid() const override { return contains(dataKey) && contains(deleteCountKey); } +}; + +class SemanticTokensDelta : public JsonObject +{ +public: + using JsonObject::JsonObject; + + QString resultId() const { return typedValue<QString>(resultIdKey); } + void setResultId(const QString &resultId) { insert(resultIdKey, resultId); } + + QList<SemanticTokensEdit> edits() const { return array<SemanticTokensEdit>(editsKey); } + void setEdits(const QList<SemanticTokensEdit> &edits) { insertArray(editsKey, edits); } + + bool isValid() const override { return contains(resultIdKey) && contains(editsKey); } +}; + +class LANGUAGESERVERPROTOCOL_EXPORT SemanticTokensDeltaResult + : public Utils::variant<SemanticTokens, SemanticTokensDelta, nullptr_t> +{ +public: + using variant::variant; + explicit SemanticTokensDeltaResult(const QJsonValue &value); +}; + +class LANGUAGESERVERPROTOCOL_EXPORT SemanticTokensFullDeltaRequest + : public Request<SemanticTokensDeltaResult, nullptr_t, SemanticTokensDeltaParams> +{ +public: + explicit SemanticTokensFullDeltaRequest(const SemanticTokensDeltaParams ¶ms); + using Request::Request; + constexpr static const char methodName[] = "textDocument/semanticTokens/full/delta"; +}; + +class LANGUAGESERVERPROTOCOL_EXPORT SemanticTokensRangeRequest + : public Request<SemanticTokensResult, nullptr_t, SemanticTokensRangeParams> +{ +public: + explicit SemanticTokensRangeRequest(const SemanticTokensRangeParams ¶ms); + using Request::Request; + constexpr static const char methodName[] = "textDocument/semanticTokens/range"; +}; + +} // namespace LanguageServerProtocol diff --git a/src/libs/languageserverprotocol/servercapabilities.cpp b/src/libs/languageserverprotocol/servercapabilities.cpp index 839f37c7f3..ba8a6836ac 100644 --- a/src/libs/languageserverprotocol/servercapabilities.cpp +++ b/src/libs/languageserverprotocol/servercapabilities.cpp @@ -165,6 +165,17 @@ void ServerCapabilities::setDocumentSymbolProvider( documentSymbolProvider); } +Utils::optional<SemanticTokensOptions> ServerCapabilities::semanticTokensProvider() const +{ + return optionalValue<SemanticTokensOptions>(semanticTokensProviderKey); +} + +void ServerCapabilities::setSemanticTokensProvider( + const SemanticTokensOptions &semanticTokensProvider) +{ + insert(semanticTokensProviderKey, semanticTokensProvider); +} + Utils::optional<Utils::variant<bool, WorkDoneProgressOptions>> ServerCapabilities::workspaceSymbolProvider() const { @@ -349,4 +360,56 @@ bool CodeActionOptions::isValid() const return WorkDoneProgressOptions::isValid() && contains(codeActionKindsKey); } +Utils::optional<Utils::variant<bool, QJsonObject>> SemanticTokensOptions::range() const +{ + using RetType = Utils::variant<bool, QJsonObject>; + const QJsonValue &rangeOptions = value(rangeKey); + if (rangeOptions.isBool()) + return RetType(rangeOptions.toBool()); + if (rangeOptions.isObject()) + return RetType(rangeOptions.toObject()); + return Utils::nullopt; +} + +void SemanticTokensOptions::setRange(const Utils::variant<bool, QJsonObject> &range) +{ + insertVariant<bool, QJsonObject>(rangeKey, range); +} + +Utils::optional<Utils::variant<bool, SemanticTokensOptions::FullSemanticTokenOptions>> +SemanticTokensOptions::full() const +{ + using RetType = Utils::variant<bool, SemanticTokensOptions::FullSemanticTokenOptions>; + const QJsonValue &fullOptions = value(fullKey); + if (fullOptions.isBool()) + return RetType(fullOptions.toBool()); + if (fullOptions.isObject()) + return RetType(FullSemanticTokenOptions(fullOptions.toObject())); + return Utils::nullopt; +} + +void SemanticTokensOptions::setFull( + const Utils::variant<bool, SemanticTokensOptions::FullSemanticTokenOptions> &full) +{ + insertVariant<bool, FullSemanticTokenOptions>(fullKey, full); +} + +SemanticRequestTypes SemanticTokensOptions::supportedRequests() const +{ + SemanticRequestTypes result; + QJsonValue rangeValue = value(rangeKey); + if (rangeValue.isObject() || rangeValue.toBool()) + result |= SemanticRequestType::Range; + QJsonValue fullValue = value(fullKey); + if (fullValue.isObject()) { + SemanticTokensOptions::FullSemanticTokenOptions options(fullValue.toObject()); + if (options.delta().value_or(false)) + result |= SemanticRequestType::FullDelta; + result |= SemanticRequestType::Full; + } else if (fullValue.toBool()) { + result |= SemanticRequestType::Full; + } + return result; +} + } // namespace LanguageServerProtocol diff --git a/src/libs/languageserverprotocol/servercapabilities.h b/src/libs/languageserverprotocol/servercapabilities.h index 8d0e80bba5..e1c847ccaa 100644 --- a/src/libs/languageserverprotocol/servercapabilities.h +++ b/src/libs/languageserverprotocol/servercapabilities.h @@ -26,6 +26,7 @@ #pragma once #include "lsptypes.h" +#include "semantictokens.h" namespace LanguageServerProtocol { @@ -133,6 +134,49 @@ public: bool isValid() const override; }; +enum class SemanticRequestType { + None = 0x0, + Full = 0x1, + FullDelta = 0x2, + Range = 0x4 +}; +Q_DECLARE_FLAGS(SemanticRequestTypes, SemanticRequestType) + +class LANGUAGESERVERPROTOCOL_EXPORT SemanticTokensOptions : public WorkDoneProgressOptions +{ +public: + using WorkDoneProgressOptions::WorkDoneProgressOptions; + + /// The legend used by the server + SemanticTokensLegend legend() const { return typedValue<SemanticTokensLegend>(legendKey); } + void setLegend(const SemanticTokensLegend &legend) { insert(legendKey, legend); } + + /// Server supports providing semantic tokens for a specific range of a document. + Utils::optional<Utils::variant<bool, QJsonObject>> range() const; + void setRange(const Utils::variant<bool, QJsonObject> &range); + void clearRange() { remove(rangeKey); } + + class FullSemanticTokenOptions : public JsonObject + { + public: + using JsonObject::JsonObject; + + /// The server supports deltas for full documents. + Utils::optional<bool> delta() const { return optionalValue<bool>(deltaKey); } + void setDelta(bool delta) { insert(deltaKey, delta); } + void clearDelta() { remove(deltaKey); } + }; + + /// Server supports providing semantic tokens for a full document. + Utils::optional<Utils::variant<bool, FullSemanticTokenOptions>> full() const; + void setFull(const Utils::variant<bool, FullSemanticTokenOptions> &full); + void clearFull() { remove(fullKey); } + + bool isValid() const override { return contains(legendKey); } + + SemanticRequestTypes supportedRequests() const; +}; + class LANGUAGESERVERPROTOCOL_EXPORT ServerCapabilities : public JsonObject { public: @@ -313,6 +357,10 @@ public: void setDocumentSymbolProvider(Utils::variant<bool, WorkDoneProgressOptions> documentSymbolProvider); void clearDocumentSymbolProvider() { remove(documentSymbolProviderKey); } + Utils::optional<SemanticTokensOptions> semanticTokensProvider() const; + void setSemanticTokensProvider(const SemanticTokensOptions &semanticTokensProvider); + void clearSemanticTokensProvider() { remove(semanticTokensProviderKey); } + // The server provides workspace symbol support. Utils::optional<Utils::variant<bool, WorkDoneProgressOptions>> workspaceSymbolProvider() const; void setWorkspaceSymbolProvider(Utils::variant<bool, WorkDoneProgressOptions> workspaceSymbolProvider); diff --git a/src/plugins/languageclient/client.cpp b/src/plugins/languageclient/client.cpp index 649064951b..10bf1540c9 100644 --- a/src/plugins/languageclient/client.cpp +++ b/src/plugins/languageclient/client.cpp @@ -81,6 +81,7 @@ Client::Client(BaseClientInterface *clientInterface) , m_documentSymbolCache(this) , m_hoverHandler(this) , m_symbolSupport(this) + , m_tokentSupport(this) { m_clientProviders.completionAssistProvider = new LanguageClientCompletionAssistProvider(this); m_clientProviders.functionHintProvider = new FunctionHintAssistProvider(this); @@ -100,6 +101,9 @@ Client::Client(BaseClientInterface *clientInterface) &TextEditor::TextEditorSettings::fontSettingsChanged, this, &Client::rehighlight); + + m_tokentSupport.setTokenTypesMap(SemanticTokens::defaultTokenTypesMap()); + m_tokentSupport.setTokenModifiersMap(SemanticTokens::defaultTokenModifiersMap()); } QString Client::name() const @@ -160,6 +164,7 @@ static ClientCapabilities generateClientCapabilities() allowDynamicRegistration.setDynamicRegistration(true); workspaceCapabilities.setDidChangeConfiguration(allowDynamicRegistration); workspaceCapabilities.setExecuteCommand(allowDynamicRegistration); + workspaceCapabilities.setConfiguration(true); capabilities.setWorkspace(workspaceCapabilities); TextDocumentClientCapabilities documentCapabilities; @@ -248,6 +253,29 @@ static ClientCapabilities generateClientCapabilities() documentCapabilities.setFormatting(allowDynamicRegistration); documentCapabilities.setRangeFormatting(allowDynamicRegistration); documentCapabilities.setOnTypeFormatting(allowDynamicRegistration); + SemanticTokensClientCapabilities tokens; + tokens.setDynamicRegistration(true); + FullSemanticTokenOptions tokenOptions; + tokenOptions.setDelta(true); + SemanticTokensClientCapabilities::Requests tokenRequests; + tokenRequests.setFull(tokenOptions); + tokens.setRequests(tokenRequests); + tokens.setTokenTypes({"type", + "class", + "enumMember", + "typeParameter", + "parameter", + "variable", + "function", + "macro", + "keyword", + "comment", + "string", + "number", + "operator"}); + tokens.setTokenModifiers({"declaration", "definition"}); + tokens.setFormats({"relative"}); + documentCapabilities.setSemanticTokens(tokens); capabilities.setTextDocument(documentCapabilities); WindowClientClientCapabilities window; @@ -312,6 +340,7 @@ void Client::openDocument(TextEditor::TextDocument *document) return; m_openedDocument[document] = document->plainText(); + if (m_state != Initialized) return; @@ -506,6 +535,7 @@ void Client::activateDocument(TextEditor::TextDocument *document) auto uri = DocumentUri::fromFilePath(document->filePath()); m_diagnosticManager.showDiagnostics(uri); SemanticHighligtingSupport::applyHighlight(document, m_highlights.value(uri), capabilities()); + m_tokentSupport.updateSemanticTokens(document); // only replace the assist provider if the language server support it updateCompletionProvider(document); updateFunctionHintProvider(document); @@ -678,6 +708,13 @@ void Client::registerCapabilities(const QList<Registration> ®istrations) for (auto document : m_openedDocument.keys()) updateFunctionHintProvider(document); } + if (registration.method() == "textDocument/semanticTokens") { + SemanticTokensOptions options(registration.registerOptions()); + if (options.isValid()) + m_tokentSupport.setLegend(options.legend()); + for (auto document : m_openedDocument.keys()) + m_tokentSupport.updateSemanticTokens(document); + } } emit capabilitiesChanged(m_dynamicCapabilities); } @@ -694,6 +731,10 @@ void Client::unregisterCapabilities(const QList<Unregistration> &unregistrations for (auto document : m_openedDocument.keys()) updateFunctionHintProvider(document); } + if (unregistration.method() == "textDocument/semanticTokens") { + for (auto document : m_openedDocument.keys()) + m_tokentSupport.updateSemanticTokens(document); + } } emit capabilitiesChanged(m_dynamicCapabilities); } @@ -1087,6 +1128,8 @@ void Client::sendPostponedDocumentUpdates() if (currentWidget && currentWidget->textDocument() == update.document) cursorPositionChanged(currentWidget); + + m_tokentSupport.updateSemanticTokens(update.document); } } @@ -1304,6 +1347,10 @@ void Client::initializeCallback(const InitializeRequest::Response &initResponse) .value_or(ServerCapabilities::SignatureHelpOptions()) .triggerCharacters()); } + auto tokenProvider = m_serverCapabilities.semanticTokensProvider().value_or( + SemanticTokensOptions()); + if (tokenProvider.isValid()) + m_tokentSupport.setLegend(tokenProvider.legend()); qCDebug(LOGLSPCLIENT) << "language server " << m_displayName << " initialized"; m_state = Initialized; diff --git a/src/plugins/languageclient/client.h b/src/plugins/languageclient/client.h index 776f8856e2..734ffd4e96 100644 --- a/src/plugins/languageclient/client.h +++ b/src/plugins/languageclient/client.h @@ -37,6 +37,7 @@ #include "languageclientsettings.h" #include "languageclientsymbolsupport.h" #include "progressmanager.h" +#include "semantichighlightsupport.h" #include <coreplugin/messagemanager.h> @@ -49,6 +50,7 @@ #include <languageserverprotocol/languagefeatures.h> #include <languageserverprotocol/messages.h> #include <languageserverprotocol/progresssupport.h> +#include <languageserverprotocol/semantictokens.h> #include <languageserverprotocol/shutdownmessages.h> #include <languageserverprotocol/textsynchronization.h> @@ -201,6 +203,9 @@ private: void updateFunctionHintProvider(TextEditor::TextDocument *document); void requestDocumentHighlights(TextEditor::TextEditorWidget *widget); + LanguageServerProtocol::SemanticRequestTypes supportedSemanticRequests(TextEditor::TextDocument *document) const; + void requestSemanticTokens(TextEditor::TextEditorWidget *widget); + void handleSemanticTokens(const LanguageServerProtocol::SemanticTokens &tokens); void rehighlight(); using ContentHandler = std::function<void(const QByteArray &, QTextCodec *, QString &, @@ -244,6 +249,7 @@ private: SymbolSupport m_symbolSupport; ProgressManager m_progressManager; bool m_activateDocAutomatically = false; + SemanticTokenSupport m_tokentSupport; }; } // namespace LanguageClient diff --git a/src/plugins/languageclient/languageclientmanager.cpp b/src/plugins/languageclient/languageclientmanager.cpp index d07f074bf6..6a1b8ff5a8 100644 --- a/src/plugins/languageclient/languageclientmanager.cpp +++ b/src/plugins/languageclient/languageclientmanager.cpp @@ -458,9 +458,9 @@ void LanguageClientManager::documentOpened(Core::IDocument *document) // check whether we have to start servers for this document const QList<BaseSettings *> settings = currentSettings(); for (BaseSettings *setting : settings) { - QVector<Client *> clients = clientForSetting(setting); if (setting->isValid() && setting->m_enabled && setting->m_languageFilter.isSupported(document)) { + QVector<Client *> clients = clientForSetting(setting); if (setting->m_startBehavior == BaseSettings::RequiresProject) { const Utils::FilePath &filePath = document->filePath(); for (ProjectExplorer::Project *project : @@ -475,12 +475,14 @@ void LanguageClientManager::documentOpened(Core::IDocument *document) return client->project() == project; }); - if (!clientForProject) { + if (!clientForProject) clientForProject = startClient(setting, project); - clients << clientForProject; - } + QTC_ASSERT(clientForProject, continue); openDocumentWithClient(textDocument, clientForProject); + // Since we already opened the document in this client we remove the client + // from the list of clients that receive the openDocument call + clients.removeAll(clientForProject); } } else if (setting->m_startBehavior == BaseSettings::RequiresFile && clients.isEmpty()) { clients << startClient(setting); diff --git a/src/plugins/languageclient/semantichighlightsupport.cpp b/src/plugins/languageclient/semantichighlightsupport.cpp index 3be55608ec..d1326ee8a9 100644 --- a/src/plugins/languageclient/semantichighlightsupport.cpp +++ b/src/plugins/languageclient/semantichighlightsupport.cpp @@ -25,9 +25,18 @@ #include "semantichighlightsupport.h" +#include "client.h" +#include "languageclientmanager.h" + +#include <texteditor/fontsettings.h> +#include <texteditor/texteditor.h> +#include <texteditor/texteditorsettings.h> +#include <utils/mimetypes/mimedatabase.h> + #include <QTextDocument> using namespace LanguageServerProtocol; +using namespace TextEditor; namespace LanguageClient { namespace SemanticHighligtingSupport { @@ -41,7 +50,7 @@ static const QList<QList<QString>> highlightScopes(const ServerCapabilities &cap .scopes().value_or(QList<QList<QString>>()); } -static Utils::optional<TextEditor::TextStyle> styleForScopes(const QList<QString> &scopes) +static Utils::optional<TextStyle> styleForScopes(const QList<QString> &scopes) { // missing "Minimal Scope Coverage" scopes @@ -63,24 +72,24 @@ static Utils::optional<TextEditor::TextStyle> styleForScopes(const QList<QString // invalid // invalid.deprecated - static const QMap<QString, TextEditor::TextStyle> styleForScopes = { - {"entity.name", TextEditor::C_TYPE}, - {"entity.name.function", TextEditor::C_FUNCTION}, - {"entity.name.function.method.static", TextEditor::C_GLOBAL}, - {"entity.name.function.preprocessor", TextEditor::C_PREPROCESSOR}, - {"entity.name.label", TextEditor::C_LABEL}, - {"keyword", TextEditor::C_KEYWORD}, - {"storage.type", TextEditor::C_KEYWORD}, - {"constant.numeric", TextEditor::C_NUMBER}, - {"string", TextEditor::C_STRING}, - {"comment", TextEditor::C_COMMENT}, - {"comment.block.documentation", TextEditor::C_DOXYGEN_COMMENT}, - {"variable.function", TextEditor::C_FUNCTION}, - {"variable.other", TextEditor::C_LOCAL}, - {"variable.other.member", TextEditor::C_FIELD}, - {"variable.other.field", TextEditor::C_FIELD}, - {"variable.other.field.static", TextEditor::C_GLOBAL}, - {"variable.parameter", TextEditor::C_PARAMETER}, + static const QMap<QString, TextStyle> styleForScopes = { + {"entity.name", C_TYPE}, + {"entity.name.function", C_FUNCTION}, + {"entity.name.function.method.static", C_GLOBAL}, + {"entity.name.function.preprocessor", C_PREPROCESSOR}, + {"entity.name.label", C_LABEL}, + {"keyword", C_KEYWORD}, + {"storage.type", C_KEYWORD}, + {"constant.numeric", C_NUMBER}, + {"string", C_STRING}, + {"comment", C_COMMENT}, + {"comment.block.documentation", C_DOXYGEN_COMMENT}, + {"variable.function", C_FUNCTION}, + {"variable.other", C_LOCAL}, + {"variable.other.member", C_FIELD}, + {"variable.other.field", C_FIELD}, + {"variable.other.field.static", C_GLOBAL}, + {"variable.parameter", C_PARAMETER}, }; for (QString scope : scopes) { @@ -98,28 +107,27 @@ static Utils::optional<TextEditor::TextStyle> styleForScopes(const QList<QString } static QHash<int, QTextCharFormat> scopesToFormatHash(QList<QList<QString>> scopes, - const TextEditor::FontSettings &fontSettings) + const FontSettings &fontSettings) { QHash<int, QTextCharFormat> scopesToFormat; for (int i = 0; i < scopes.size(); ++i) { - if (Utils::optional<TextEditor::TextStyle> style = styleForScopes(scopes[i])) + if (Utils::optional<TextStyle> style = styleForScopes(scopes[i])) scopesToFormat[i] = fontSettings.toTextCharFormat(style.value()); } return scopesToFormat; } -TextEditor::HighlightingResult tokenToHighlightingResult(int line, - const SemanticHighlightToken &token) +HighlightingResult tokenToHighlightingResult(int line, const SemanticHighlightToken &token) { - return TextEditor::HighlightingResult(unsigned(line) + 1, - unsigned(token.character) + 1, - token.length, - int(token.scope)); + return HighlightingResult(unsigned(line) + 1, + unsigned(token.character) + 1, + token.length, + int(token.scope)); } -TextEditor::HighlightingResults generateResults(const QList<SemanticHighlightingInformation> &lines) +HighlightingResults generateResults(const QList<SemanticHighlightingInformation> &lines) { - TextEditor::HighlightingResults results; + HighlightingResults results; for (const SemanticHighlightingInformation &info : lines) { const int line = info.line(); @@ -132,8 +140,8 @@ TextEditor::HighlightingResults generateResults(const QList<SemanticHighlighting return results; } -void applyHighlight(TextEditor::TextDocument *doc, - const TextEditor::HighlightingResults &results, +void applyHighlight(TextDocument *doc, + const HighlightingResults &results, const ServerCapabilities &capabilities) { if (!doc->syntaxHighlighter()) @@ -145,7 +153,7 @@ void applyHighlight(TextEditor::TextDocument *doc, auto b = doc->document()->findBlockByNumber(int(result.line - 1)); const QString &text = b.text().mid(int(result.column - 1), int(result.length)); auto resultScupes = scopes[result.kind]; - auto style = styleForScopes(resultScupes).value_or(TextEditor::C_TEXT); + auto style = styleForScopes(resultScupes).value_or(C_TEXT); qCDebug(LOGLSPHIGHLIGHT) << result.line - 1 << '\t' << result.column - 1 << '\t' << result.length << '\t' @@ -156,7 +164,7 @@ void applyHighlight(TextEditor::TextDocument *doc, } if (capabilities.semanticHighlighting().has_value()) { - TextEditor::SemanticHighlighter::setExtraAdditionalFormats( + SemanticHighlighter::setExtraAdditionalFormats( doc->syntaxHighlighter(), results, scopesToFormatHash(highlightScopes(capabilities), doc->fontSettings())); @@ -164,4 +172,269 @@ void applyHighlight(TextEditor::TextDocument *doc, } } // namespace SemanticHighligtingSupport + +constexpr int tokenTypeBitOffset = 16; + +SemanticTokenSupport::SemanticTokenSupport(Client *client) + : m_client(client) +{ + QObject::connect(TextEditorSettings::instance(), + &TextEditorSettings::fontSettingsChanged, + client, + [this]() { updateFormatHash(); }); +} + +void SemanticTokenSupport::reloadSemanticTokens(TextDocument *textDocument) +{ + const SemanticRequestTypes supportedRequests = supportedSemanticRequests(textDocument); + if (supportedRequests.testFlag(SemanticRequestType::None)) + return; + const Utils::FilePath filePath = textDocument->filePath(); + const TextDocumentIdentifier docId(DocumentUri::fromFilePath(filePath)); + auto responseCallback = [this, filePath](const SemanticTokensFullRequest::Response &response){ + handleSemanticTokens(filePath, response.result().value_or(nullptr)); + }; + /*if (supportedRequests.testFlag(SemanticRequestType::Range)) { + const int start = widget->firstVisibleBlockNumber(); + const int end = widget->lastVisibleBlockNumber(); + const int pageSize = end - start; + // request one extra page upfront and after the current visible range + Range range(Position(qMax(0, start - pageSize), 0), + Position(qMin(widget->blockCount() - 1, end + pageSize), 0)); + SemanticTokensRangeParams params; + params.setTextDocument(docId); + params.setRange(range); + SemanticTokensRangeRequest request(params); + request.setResponseCallback(responseCallback); + m_client->sendContent(request); + } else */ + if (supportedRequests.testFlag(SemanticRequestType::Full)) { + SemanticTokensParams params; + params.setTextDocument(docId); + SemanticTokensFullRequest request(params); + request.setResponseCallback(responseCallback); + m_client->sendContent(request); + } +} + +void SemanticTokenSupport::updateSemanticTokens(TextDocument *textDocument) +{ + const SemanticRequestTypes supportedRequests = supportedSemanticRequests(textDocument); + if (supportedRequests.testFlag(SemanticRequestType::FullDelta)) { + const Utils::FilePath filePath = textDocument->filePath(); + const QString &previousResultId = m_tokens.value(filePath).resultId().value_or(QString()); + if (!previousResultId.isEmpty()) { + SemanticTokensDeltaParams params; + params.setTextDocument(TextDocumentIdentifier(DocumentUri::fromFilePath(filePath))); + params.setPreviousResultId(previousResultId); + SemanticTokensFullDeltaRequest request(params); + request.setResponseCallback( + [this, filePath](const SemanticTokensFullDeltaRequest::Response &response) { + handleSemanticTokensDelta(filePath, response.result().value_or(nullptr)); + }); + m_client->sendContent(request); + return; + } + } + reloadSemanticTokens(textDocument); +} + +void SemanticTokenSupport::rehighlight() +{ + for (const Utils::FilePath &filePath : m_tokens.keys()) + highlight(filePath); +} + +void addModifiers(int key, + QHash<int, QTextCharFormat> *formatHash, + TextStyles styles, + QList<int> tokenModifiers, + const TextEditor::FontSettings &fs) +{ + if (tokenModifiers.isEmpty()) + return; + int modifier = tokenModifiers.takeLast(); + auto addModifier = [&](TextStyle style){ + if (key & modifier) // already there don't add twice + return; + key = key | modifier; + styles.mixinStyles.push_back(style); + formatHash->insert(key, fs.toTextCharFormat(styles)); + }; + switch (modifier) { + case declarationModifier: addModifier(C_DECLARATION); break; + case definitionModifier: addModifier(C_FUNCTION_DEFINITION); break; + default: break; + } + addModifiers(key, formatHash, styles, tokenModifiers, fs); +} + +void SemanticTokenSupport::setLegend(const LanguageServerProtocol::SemanticTokensLegend &legend) +{ + m_tokenTypes = Utils::transform(legend.tokenTypes(), [&](const QString &tokenTypeString){ + return m_tokenTypesMap.value(tokenTypeString, -1); + }); + m_tokenModifiers = Utils::transform(legend.tokenModifiers(), [&](const QString &tokenModifierString){ + return m_tokenModifiersMap.value(tokenModifierString, -1); + }); + updateFormatHash(); +} + +void SemanticTokenSupport::updateFormatHash() +{ + auto fontSettings = TextEditorSettings::fontSettings(); + for (int tokenType : qAsConst(m_tokenTypes)) { + if (tokenType < 0) + continue; + TextStyle style; + switch (tokenType) { + case typeToken: style = C_TYPE; break; + case classToken: style = C_TYPE; break; + case enumMemberToken: style = C_ENUMERATION; break; + case typeParameterToken: style = C_FIELD; break; + case parameterToken: style = C_PARAMETER; break; + case variableToken: style = C_LOCAL; break; + case functionToken: style = C_FUNCTION; break; + case macroToken: style = C_PREPROCESSOR; break; + case keywordToken: style = C_KEYWORD; break; + case commentToken: style = C_COMMENT; break; + case stringToken: style = C_STRING; break; + case numberToken: style = C_NUMBER; break; + case operatorToken: style = C_OPERATOR; break; + default: + style = m_additionalTypeStyles.value(tokenType, C_TEXT); + break; + } + int mainHashPart = tokenType << tokenTypeBitOffset; + m_formatHash[mainHashPart] = fontSettings.toTextCharFormat(style); + TextStyles styles; + styles.mainStyle = style; + styles.mixinStyles.initializeElements(); + addModifiers(mainHashPart, &m_formatHash, styles, m_tokenModifiers, fontSettings); + } + rehighlight(); +} + +void SemanticTokenSupport::setTokenTypesMap(const QMap<QString, int> &tokenTypesMap) +{ + m_tokenTypesMap = tokenTypesMap; +} + +void SemanticTokenSupport::setTokenModifiersMap(const QMap<QString, int> &tokenModifiersMap) +{ + m_tokenModifiersMap = tokenModifiersMap; +} + +void SemanticTokenSupport::setAdditionalTokenTypeStyles( + const QHash<int, TextStyle> &typeStyles) +{ + m_additionalTypeStyles = typeStyles; +} + +//void SemanticTokenSupport::setAdditionalTokenModifierStyles( +// const QHash<int, TextStyle> &modifierStyles) +//{ +// m_additionalModifierStyles = modifierStyles; +//} + +SemanticRequestTypes SemanticTokenSupport::supportedSemanticRequests(TextDocument *document) const +{ + auto supportedRequests = [&](const QJsonObject &options) -> SemanticRequestTypes { + TextDocumentRegistrationOptions docOptions(options); + if (docOptions.isValid() + && docOptions.filterApplies(document->filePath(), + Utils::mimeTypeForName(document->mimeType()))) { + return SemanticRequestType::None; + } + const SemanticTokensOptions semanticOptions(options); + return semanticOptions.supportedRequests(); + }; + const QString dynamicMethod = "textDocument/semanticTokens"; + const DynamicCapabilities &dynamicCapabilities = m_client->dynamicCapabilities(); + if (auto registered = dynamicCapabilities.isRegistered(dynamicMethod); + registered.has_value()) { + if (!registered.value()) + return SemanticRequestType::None; + return supportedRequests(dynamicCapabilities.option(dynamicMethod).toObject()); + } + if (m_client->capabilities().semanticTokensProvider().has_value()) + return supportedRequests(m_client->capabilities().semanticTokensProvider().value()); + return SemanticRequestType::None; +} + +void SemanticTokenSupport::handleSemanticTokens(const Utils::FilePath &filePath, + const SemanticTokensResult &result) +{ + if (auto tokens = Utils::get_if<SemanticTokens>(&result)) + m_tokens[filePath] = *tokens; + else + m_tokens.remove(filePath); + highlight(filePath); +} + +void SemanticTokenSupport::handleSemanticTokensDelta( + const Utils::FilePath &filePath, const LanguageServerProtocol::SemanticTokensDeltaResult &result) +{ + if (auto tokens = Utils::get_if<SemanticTokens>(&result)) { + m_tokens[filePath] = *tokens; + } else if (auto tokensDelta = Utils::get_if<SemanticTokensDelta>(&result)) { + const QList<SemanticTokensEdit> &edits = tokensDelta->edits(); + if (edits.isEmpty()) + return; + + SemanticTokens &tokens = m_tokens[filePath]; + QList<int> data = tokens.data(); + + int newDataSize = data.size(); + for (const SemanticTokensEdit &edit : edits) + newDataSize += edit.dataSize() - edit.deleteCount(); + QList<int> newData; + newData.reserve(newDataSize); + + auto it = data.begin(); + int currentDelta = 0; + for (const SemanticTokensEdit &edit : edits) { + for (const auto start = it + edit.start() + currentDelta; it != start; ++it) + newData.append(*it); + const QList<int> insertData = edit.data().value_or(QList<int>()); + newData.append(edit.data().value_or(QList<int>())); + const int deleteCount = edit.deleteCount(); + currentDelta += insertData.size() - deleteCount; + it += edit.deleteCount(); + } + for (const auto end = data.end(); it != end; ++it) + newData.append(*it); + + tokens.setData(newData); + tokens.setResultId(tokensDelta->resultId()); + } else { + m_tokens.remove(filePath); + } + highlight(filePath); +} + +void SemanticTokenSupport::highlight(const Utils::FilePath &filePath) +{ + TextDocument *doc = TextDocument::textDocumentForFilePath(filePath); + if (!doc || LanguageClientManager::clientForDocument(doc) != m_client) + return; + SyntaxHighlighter *highlighter = doc->syntaxHighlighter(); + if (!highlighter) + return; + int line = 1; + int column = 1; + auto toResult = [&](const SemanticToken &token){ + line += token.deltaLine; + if (token.deltaLine != 0) // reset the current column when we change the current line + column = 1; + column += token.deltaStart; + const int tokenKind = token.tokenType << tokenTypeBitOffset | token.tokenModifiers; + return HighlightingResult(line, column, token.length, tokenKind); + }; + const QList<SemanticToken> tokens = m_tokens.value(filePath).toTokens(m_tokenTypes, + m_tokenModifiers); + const HighlightingResults results = Utils::transform(tokens, toResult); + SemanticHighlighter::setExtraAdditionalFormats(highlighter, results, m_formatHash); +} + } // namespace LanguageClient diff --git a/src/plugins/languageclient/semantichighlightsupport.h b/src/plugins/languageclient/semantichighlightsupport.h index b9d4fe1555..0b7693ad66 100644 --- a/src/plugins/languageclient/semantichighlightsupport.h +++ b/src/plugins/languageclient/semantichighlightsupport.h @@ -32,7 +32,11 @@ #include <texteditor/semantichighlighter.h> #include <texteditor/textdocument.h> +#include <QTextCharFormat> + namespace LanguageClient { +class Client; + namespace SemanticHighligtingSupport { TextEditor::HighlightingResults generateResults( @@ -43,4 +47,46 @@ void applyHighlight(TextEditor::TextDocument *doc, const LanguageServerProtocol::ServerCapabilities &capabilities); } // namespace SemanticHighligtingSupport + +class SemanticTokenSupport +{ +public: + explicit SemanticTokenSupport(Client *client); + + void reloadSemanticTokens(TextEditor::TextDocument *doc); + void updateSemanticTokens(TextEditor::TextDocument *doc); + void rehighlight(); + void setLegend(const LanguageServerProtocol::SemanticTokensLegend &legend); + + void setTokenTypesMap(const QMap<QString, int> &tokenTypesMap); + void setTokenModifiersMap(const QMap<QString, int> &tokenModifiersMap); + + void setAdditionalTokenTypeStyles(const QHash<int, TextEditor::TextStyle> &typeStyles); + // TODO: currently only declaration and definition modifiers are supported. The TextStyles + // mixin capabilities need to be extended to be able to support more +// void setAdditionalTokenModifierStyles(const QHash<int, TextEditor::TextStyle> &modifierStyles); + +private: + LanguageServerProtocol::SemanticRequestTypes supportedSemanticRequests( + TextEditor::TextDocument *document) const; + void handleSemanticTokens(const Utils::FilePath &filePath, + const LanguageServerProtocol::SemanticTokensResult &result); + void handleSemanticTokensDelta(const Utils::FilePath &filePath, + const LanguageServerProtocol::SemanticTokensDeltaResult &result); + void highlight(const Utils::FilePath &filePath); + void updateFormatHash(); + void currentEditorChanged(); + + Client *m_client = nullptr; + + QHash<Utils::FilePath, LanguageServerProtocol::SemanticTokens> m_tokens; + QList<int> m_tokenTypes; + QList<int> m_tokenModifiers; + QHash<int, QTextCharFormat> m_formatHash; + QHash<int, TextEditor::TextStyle> m_additionalTypeStyles; +// QHash<int, TextEditor::TextStyle> m_additionalModifierStyles; + QMap<QString, int> m_tokenTypesMap; + QMap<QString, int> m_tokenModifiersMap; +}; + } // namespace LanguageClient |
