| 1 | // Copyright (C) 2024 The Qt Company Ltd. | 
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only | 
| 3 |  | 
| 4 | #include <qqmlhighlightsupport_p.h> | 
| 5 |  | 
| 6 | QT_BEGIN_NAMESPACE | 
| 7 |  | 
| 8 | using namespace Qt::StringLiterals; | 
| 9 | using namespace QLspSpecification; | 
| 10 | using namespace QQmlJS::Dom; | 
| 11 |  | 
| 12 | /*! | 
| 13 | \internal | 
| 14 | Make a list of enum names to register the supported token | 
| 15 | types and modifiers. It is case-sensitive in the protocol | 
| 16 | thus we need to lower the first characters of the enum names. | 
| 17 | */ | 
| 18 | template <typename EnumType> | 
| 19 | static QList<QByteArray> enumToByteArray() | 
| 20 | { | 
| 21 |     QList<QByteArray> result; | 
| 22 |     QMetaEnum metaEnum = QMetaEnum::fromType<EnumType>(); | 
| 23 |     for (auto i = 0; i < metaEnum.keyCount(); ++i) { | 
| 24 |         auto &&enumName = QByteArray(metaEnum.key(index: i)); | 
| 25 |         enumName.front() = std::tolower(c: enumName.front()); | 
| 26 |         result.emplace_back(args: std::move(enumName)); | 
| 27 |     } | 
| 28 |  | 
| 29 |     return result; | 
| 30 | } | 
| 31 |  | 
| 32 | QList<QByteArray> defaultTokenModifiersList() | 
| 33 | { | 
| 34 |     return enumToByteArray<QLspSpecification::SemanticTokenModifiers>(); | 
| 35 | } | 
| 36 |  | 
| 37 | QList<QByteArray> extendedTokenTypesList() | 
| 38 | { | 
| 39 |     return enumToByteArray<HighlightingUtils::SemanticTokenProtocolTypes>(); | 
| 40 | } | 
| 41 |  | 
| 42 | /*! | 
| 43 | \internal | 
| 44 | A wrapper class that handles the semantic tokens request for a whole file as described in | 
| 45 | https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/#semanticTokens_fullRequest | 
| 46 | Sends a QLspSpecification::SemanticTokens data as response that is generated for the entire file. | 
| 47 | */ | 
| 48 | SemanticTokenFullHandler::SemanticTokenFullHandler(QmlLsp::QQmlCodeModel *codeModel) | 
| 49 |     : QQmlBaseModule(codeModel), m_mode(HighlightingUtils::HighlightingMode::Default) | 
| 50 | { | 
| 51 | } | 
| 52 |  | 
| 53 | void SemanticTokenFullHandler::process( | 
| 54 |         QQmlBaseModule<SemanticTokensRequest>::RequestPointerArgument request) | 
| 55 | { | 
| 56 |     if (!request) { | 
| 57 |         qCWarning(semanticTokens) << "No semantic token request is available!" ; | 
| 58 |         return; | 
| 59 |     } | 
| 60 |  | 
| 61 |     Responses::SemanticTokensResultType result; | 
| 62 |     ResponseScopeGuard guard(result, request->m_response); | 
| 63 |     const auto doc = m_codeModel->openDocumentByUrl( | 
| 64 |             url: QQmlLSUtils::lspUriToQmlUrl(uri: request->m_parameters.textDocument.uri)); | 
| 65 |     DomItem file = doc.snapshot.doc.fileObject(option: GoTo::MostLikely); | 
| 66 |     const auto fileObject = file.ownerAs<QmlFile>(); | 
| 67 |     if (!fileObject || !(fileObject && fileObject->isValid())) { | 
| 68 |         guard.setError({ | 
| 69 |                 .code: int(QLspSpecification::ErrorCodes::RequestCancelled), | 
| 70 |                 .message: "Cannot proceed: current QML document is invalid!"_L1 , | 
| 71 |         }); | 
| 72 |         return; | 
| 73 |     } | 
| 74 |     auto &&encoded = HighlightingUtils::collectTokens(item: file, range: std::nullopt, mode: m_mode); | 
| 75 |     auto ®isteredTokens = m_codeModel->registeredTokens(); | 
| 76 |     if (!encoded.isEmpty()) { | 
| 77 |         HighlightingUtils::updateResultID(resultID&: registeredTokens.resultId); | 
| 78 |         result = SemanticTokens{ .resultId: registeredTokens.resultId, .data: encoded }; | 
| 79 |         registeredTokens.lastTokens = std::move(encoded); | 
| 80 |     } else { | 
| 81 |         result = nullptr; | 
| 82 |     } | 
| 83 | } | 
| 84 |  | 
| 85 | void SemanticTokenFullHandler::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol) | 
| 86 | { | 
| 87 |     protocol->registerSemanticTokensRequestHandler(handler: getRequestHandler()); | 
| 88 | } | 
| 89 |  | 
| 90 | /*! | 
| 91 | \internal | 
| 92 | A wrapper class that handles the semantic tokens delta request for a file | 
| 93 | https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/#semanticTokens_deltaRequest | 
| 94 | Sends either SemanticTokens or SemanticTokensDelta data as response. | 
| 95 | This is generally requested when the text document is edited after receiving full highlighting data. | 
| 96 | */ | 
| 97 | SemanticTokenDeltaHandler::SemanticTokenDeltaHandler(QmlLsp::QQmlCodeModel *codeModel) | 
| 98 |     : QQmlBaseModule(codeModel), m_mode(HighlightingUtils::HighlightingMode::Default) | 
| 99 | { | 
| 100 | } | 
| 101 |  | 
| 102 | void SemanticTokenDeltaHandler::process( | 
| 103 |         QQmlBaseModule<SemanticTokensDeltaRequest>::RequestPointerArgument request) | 
| 104 | { | 
| 105 |     if (!request) { | 
| 106 |         qCWarning(semanticTokens) << "No semantic token request is available!" ; | 
| 107 |         return; | 
| 108 |     } | 
| 109 |  | 
| 110 |     Responses::SemanticTokensDeltaResultType result; | 
| 111 |     ResponseScopeGuard guard(result, request->m_response); | 
| 112 |     const auto doc = m_codeModel->openDocumentByUrl( | 
| 113 |             url: QQmlLSUtils::lspUriToQmlUrl(uri: request->m_parameters.textDocument.uri)); | 
| 114 |     DomItem file = doc.snapshot.doc.fileObject(option: GoTo::MostLikely); | 
| 115 |     const auto fileObject = file.ownerAs<QmlFile>(); | 
| 116 |     if (!fileObject || !(fileObject && fileObject->isValid())) { | 
| 117 |         guard.setError({ | 
| 118 |                 .code: int(QLspSpecification::ErrorCodes::RequestCancelled), | 
| 119 |                 .message: "Cannot proceed: current QML document is invalid!"_L1 , | 
| 120 |         }); | 
| 121 |         return; | 
| 122 |     } | 
| 123 |     auto newEncoded = HighlightingUtils::collectTokens(item: file, range: std::nullopt, mode: m_mode); | 
| 124 |     auto ®isteredTokens = m_codeModel->registeredTokens(); | 
| 125 |     const auto lastResultId = registeredTokens.resultId; | 
| 126 |     HighlightingUtils::updateResultID(resultID&: registeredTokens.resultId); | 
| 127 |  | 
| 128 |     // Return full token list if result ids not align | 
| 129 |     // otherwise compute the delta. | 
| 130 |     if (lastResultId == request->m_parameters.previousResultId) { | 
| 131 |         result = QLspSpecification::SemanticTokensDelta{ | 
| 132 |             .resultId: registeredTokens.resultId, | 
| 133 |             .edits: HighlightingUtils::computeDiff(registeredTokens.lastTokens, newEncoded) | 
| 134 |         }; | 
| 135 |     } else if (!newEncoded.isEmpty()) { | 
| 136 |         result = QLspSpecification::SemanticTokens{ .resultId: registeredTokens.resultId, .data: newEncoded }; | 
| 137 |     } else { | 
| 138 |         result = nullptr; | 
| 139 |     } | 
| 140 |     registeredTokens.lastTokens = std::move(newEncoded); | 
| 141 | } | 
| 142 |  | 
| 143 | void SemanticTokenDeltaHandler::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol) | 
| 144 | { | 
| 145 |     protocol->registerSemanticTokensDeltaRequestHandler(handler: getRequestHandler()); | 
| 146 | } | 
| 147 |  | 
| 148 | /*! | 
| 149 | \internal | 
| 150 | A wrapper class that handles the semantic tokens range request for a file | 
| 151 | https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/#semanticTokens_rangeRequest | 
| 152 | Sends a QLspSpecification::SemanticTokens data as response that is generated for a range of file. | 
| 153 | */ | 
| 154 | SemanticTokenRangeHandler::SemanticTokenRangeHandler(QmlLsp::QQmlCodeModel *codeModel) | 
| 155 |     : QQmlBaseModule(codeModel), m_mode(HighlightingUtils::HighlightingMode::Default) | 
| 156 | { | 
| 157 | } | 
| 158 |  | 
| 159 | void SemanticTokenRangeHandler::process( | 
| 160 |         QQmlBaseModule<SemanticTokensRangeRequest>::RequestPointerArgument request) | 
| 161 | { | 
| 162 |     if (!request) { | 
| 163 |         qCWarning(semanticTokens) << "No semantic token request is available!" ; | 
| 164 |         return; | 
| 165 |     } | 
| 166 |  | 
| 167 |     Responses::SemanticTokensRangeResultType result; | 
| 168 |     ResponseScopeGuard guard(result, request->m_response); | 
| 169 |     const auto doc = m_codeModel->openDocumentByUrl( | 
| 170 |             url: QQmlLSUtils::lspUriToQmlUrl(uri: request->m_parameters.textDocument.uri)); | 
| 171 |     DomItem file = doc.snapshot.doc.fileObject(option: GoTo::MostLikely); | 
| 172 |     const auto qmlFile = file.as<QmlFile>(); | 
| 173 |     if (!qmlFile || !(qmlFile && qmlFile->isValid())) { | 
| 174 |         guard.setError({ | 
| 175 |                 .code: int(QLspSpecification::ErrorCodes::RequestCancelled), | 
| 176 |                 .message: "Cannot proceed: current QML document is invalid!"_L1 , | 
| 177 |         }); | 
| 178 |         return; | 
| 179 |     } | 
| 180 |     const QString &code = qmlFile->code(); | 
| 181 |     const auto range = request->m_parameters.range; | 
| 182 |     int startOffset = | 
| 183 |             int(QQmlLSUtils::textOffsetFrom(code, row: range.start.line, character: range.end.character)); | 
| 184 |     int endOffset = int(QQmlLSUtils::textOffsetFrom(code, row: range.end.line, character: range.end.character)); | 
| 185 |     auto &&encoded = HighlightingUtils::collectTokens( | 
| 186 |             item: file, range: HighlightsRange{ .startOffset: startOffset, .endOffset: endOffset }, mode: m_mode); | 
| 187 |     auto ®isteredTokens = m_codeModel->registeredTokens(); | 
| 188 |     if (!encoded.isEmpty()) { | 
| 189 |         HighlightingUtils::updateResultID(resultID&: registeredTokens.resultId); | 
| 190 |         result = SemanticTokens{ .resultId: registeredTokens.resultId, .data: std::move(encoded) }; | 
| 191 |     } else { | 
| 192 |         result = nullptr; | 
| 193 |     } | 
| 194 | } | 
| 195 |  | 
| 196 | void SemanticTokenRangeHandler::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol) | 
| 197 | { | 
| 198 |     protocol->registerSemanticTokensRangeRequestHandler(handler: getRequestHandler()); | 
| 199 | } | 
| 200 |  | 
| 201 | QQmlHighlightSupport::QQmlHighlightSupport(QmlLsp::QQmlCodeModel *codeModel) | 
| 202 |     : m_full(codeModel), m_delta(codeModel), m_range(codeModel) | 
| 203 | { | 
| 204 | } | 
| 205 |  | 
| 206 | QString QQmlHighlightSupport::name() const | 
| 207 | { | 
| 208 |     return "QQmlHighlightSupport"_L1 ; | 
| 209 | } | 
| 210 |  | 
| 211 | void QQmlHighlightSupport::registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) | 
| 212 | { | 
| 213 |     m_full.registerHandlers(server, protocol); | 
| 214 |     m_delta.registerHandlers(server, protocol); | 
| 215 |     m_range.registerHandlers(server, protocol); | 
| 216 | } | 
| 217 |  | 
| 218 | void QQmlHighlightSupport::setupCapabilities( | 
| 219 |         const QLspSpecification::InitializeParams &clientCapabilities, | 
| 220 |         QLspSpecification::InitializeResult &serverCapabilities) | 
| 221 | { | 
| 222 |     QLspSpecification::SemanticTokensOptions options; | 
| 223 |     options.range = true; | 
| 224 |     options.full = QJsonObject({ { u"delta"_s , true } }); | 
| 225 |  | 
| 226 |     if (auto clientInitOptions = clientCapabilities.initializationOptions) { | 
| 227 |         auto object = *clientInitOptions; | 
| 228 |         if (object[u"qtCreatorHighlighting"_s ].toBool(defaultValue: false)) { | 
| 229 |             const auto mode = HighlightingUtils::HighlightingMode::QtCHighlighting; | 
| 230 |             m_delta.setHighlightingMode(mode); | 
| 231 |             m_full.setHighlightingMode(mode); | 
| 232 |             m_range.setHighlightingMode(mode); | 
| 233 |         } | 
| 234 |     } | 
| 235 |     options.legend.tokenTypes = extendedTokenTypesList(); | 
| 236 |     options.legend.tokenModifiers = defaultTokenModifiersList(); | 
| 237 |     serverCapabilities.capabilities.semanticTokensProvider = options; | 
| 238 | } | 
| 239 |  | 
| 240 | QT_END_NAMESPACE | 
| 241 |  |