1 /****************************************************************************
2 **
3 ** Copyright (C) 2019 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25
26 #include "semantichighlightsupport.h"
27
28 #include "client.h"
29 #include "languageclientmanager.h"
30
31 #include <texteditor/fontsettings.h>
32 #include <texteditor/texteditor.h>
33 #include <texteditor/texteditorsettings.h>
34 #include <utils/mimetypes/mimedatabase.h>
35
36 #include <QTextDocument>
37
38 using namespace LanguageServerProtocol;
39 using namespace TextEditor;
40
41 namespace LanguageClient {
42
43 static Q_LOGGING_CATEGORY(LOGLSPHIGHLIGHT, "qtc.languageclient.highlight", QtWarningMsg);
44
45 namespace SemanticHighligtingSupport {
46
highlightScopes(const ServerCapabilities & capabilities)47 static const QList<QList<QString>> highlightScopes(const ServerCapabilities &capabilities)
48 {
49 return capabilities.semanticHighlighting()
50 .value_or(ServerCapabilities::SemanticHighlightingServerCapabilities())
51 .scopes().value_or(QList<QList<QString>>());
52 }
53
styleForScopes(const QList<QString> & scopes)54 static Utils::optional<TextStyle> styleForScopes(const QList<QString> &scopes)
55 {
56 // missing "Minimal Scope Coverage" scopes
57
58 // entity.other.inherited-class
59 // entity.name.section
60 // entity.name.tag
61 // entity.other.attribute-name
62 // variable.language
63 // variable.parameter
64 // variable.function
65 // constant.numeric
66 // constant.language
67 // constant.character.escape
68 // support
69 // storage.modifier
70 // keyword.control
71 // keyword.operator
72 // keyword.declaration
73 // invalid
74 // invalid.deprecated
75
76 static const QMap<QString, TextStyle> styleForScopes = {
77 {"entity.name", C_TYPE},
78 {"entity.name.function", C_FUNCTION},
79 {"entity.name.function.method.static", C_GLOBAL},
80 {"entity.name.function.preprocessor", C_PREPROCESSOR},
81 {"entity.name.label", C_LABEL},
82 {"keyword", C_KEYWORD},
83 {"storage.type", C_KEYWORD},
84 {"constant.numeric", C_NUMBER},
85 {"string", C_STRING},
86 {"comment", C_COMMENT},
87 {"comment.block.documentation", C_DOXYGEN_COMMENT},
88 {"variable.function", C_FUNCTION},
89 {"variable.other", C_LOCAL},
90 {"variable.other.member", C_FIELD},
91 {"variable.other.field", C_FIELD},
92 {"variable.other.field.static", C_GLOBAL},
93 {"variable.parameter", C_PARAMETER},
94 };
95
96 for (QString scope : scopes) {
97 while (!scope.isEmpty()) {
98 auto style = styleForScopes.find(scope);
99 if (style != styleForScopes.end())
100 return style.value();
101 const int index = scope.lastIndexOf('.');
102 if (index <= 0)
103 break;
104 scope = scope.left(index);
105 }
106 }
107 return Utils::nullopt;
108 }
109
scopesToFormatHash(QList<QList<QString>> scopes,const FontSettings & fontSettings)110 static QHash<int, QTextCharFormat> scopesToFormatHash(QList<QList<QString>> scopes,
111 const FontSettings &fontSettings)
112 {
113 QHash<int, QTextCharFormat> scopesToFormat;
114 for (int i = 0; i < scopes.size(); ++i) {
115 if (Utils::optional<TextStyle> style = styleForScopes(scopes[i]))
116 scopesToFormat[i] = fontSettings.toTextCharFormat(style.value());
117 }
118 return scopesToFormat;
119 }
120
tokenToHighlightingResult(int line,const SemanticHighlightToken & token)121 HighlightingResult tokenToHighlightingResult(int line, const SemanticHighlightToken &token)
122 {
123 return HighlightingResult(unsigned(line) + 1,
124 unsigned(token.character) + 1,
125 token.length,
126 int(token.scope));
127 }
128
generateResults(const QList<SemanticHighlightingInformation> & lines)129 HighlightingResults generateResults(const QList<SemanticHighlightingInformation> &lines)
130 {
131 HighlightingResults results;
132
133 for (const SemanticHighlightingInformation &info : lines) {
134 const int line = info.line();
135 for (const SemanticHighlightToken &token :
136 info.tokens().value_or(QList<SemanticHighlightToken>())) {
137 results << tokenToHighlightingResult(line, token);
138 }
139 }
140
141 return results;
142 }
143
applyHighlight(TextDocument * doc,const HighlightingResults & results,const ServerCapabilities & capabilities)144 void applyHighlight(TextDocument *doc,
145 const HighlightingResults &results,
146 const ServerCapabilities &capabilities)
147 {
148 if (!doc->syntaxHighlighter())
149 return;
150 if (LOGLSPHIGHLIGHT().isDebugEnabled()) {
151 auto scopes = highlightScopes(capabilities);
152 qCDebug(LOGLSPHIGHLIGHT) << "semantic highlight for" << doc->filePath();
153 for (auto result : results) {
154 auto b = doc->document()->findBlockByNumber(int(result.line - 1));
155 const QString &text = b.text().mid(int(result.column - 1), int(result.length));
156 auto resultScupes = scopes[result.kind];
157 auto style = styleForScopes(resultScupes).value_or(C_TEXT);
158 qCDebug(LOGLSPHIGHLIGHT) << result.line - 1 << '\t'
159 << result.column - 1 << '\t'
160 << result.length << '\t'
161 << TextEditor::Constants::nameForStyle(style) << '\t'
162 << text
163 << resultScupes;
164 }
165 }
166
167 if (capabilities.semanticHighlighting().has_value()) {
168 SemanticHighlighter::setExtraAdditionalFormats(
169 doc->syntaxHighlighter(),
170 results,
171 scopesToFormatHash(highlightScopes(capabilities), doc->fontSettings()));
172 }
173 }
174
175 } // namespace SemanticHighligtingSupport
176
177 constexpr int tokenTypeBitOffset = 16;
178
SemanticTokenSupport(Client * client)179 SemanticTokenSupport::SemanticTokenSupport(Client *client)
180 : m_client(client)
181 {
182 QObject::connect(TextEditorSettings::instance(),
183 &TextEditorSettings::fontSettingsChanged,
184 client,
185 [this]() { updateFormatHash(); });
186 }
187
reloadSemanticTokens(TextDocument * textDocument)188 void SemanticTokenSupport::reloadSemanticTokens(TextDocument *textDocument)
189 {
190 const SemanticRequestTypes supportedRequests = supportedSemanticRequests(textDocument);
191 if (supportedRequests.testFlag(SemanticRequestType::None))
192 return;
193 const Utils::FilePath filePath = textDocument->filePath();
194 const TextDocumentIdentifier docId(DocumentUri::fromFilePath(filePath));
195 auto responseCallback = [this, filePath](const SemanticTokensFullRequest::Response &response){
196 handleSemanticTokens(filePath, response.result().value_or(nullptr));
197 };
198 /*if (supportedRequests.testFlag(SemanticRequestType::Range)) {
199 const int start = widget->firstVisibleBlockNumber();
200 const int end = widget->lastVisibleBlockNumber();
201 const int pageSize = end - start;
202 // request one extra page upfront and after the current visible range
203 Range range(Position(qMax(0, start - pageSize), 0),
204 Position(qMin(widget->blockCount() - 1, end + pageSize), 0));
205 SemanticTokensRangeParams params;
206 params.setTextDocument(docId);
207 params.setRange(range);
208 SemanticTokensRangeRequest request(params);
209 request.setResponseCallback(responseCallback);
210 m_client->sendContent(request);
211 } else */
212 if (supportedRequests.testFlag(SemanticRequestType::Full)) {
213 SemanticTokensParams params;
214 params.setTextDocument(docId);
215 SemanticTokensFullRequest request(params);
216 request.setResponseCallback(responseCallback);
217 m_client->sendContent(request);
218 }
219 }
220
updateSemanticTokens(TextDocument * textDocument)221 void SemanticTokenSupport::updateSemanticTokens(TextDocument *textDocument)
222 {
223 const SemanticRequestTypes supportedRequests = supportedSemanticRequests(textDocument);
224 if (supportedRequests.testFlag(SemanticRequestType::FullDelta)) {
225 const Utils::FilePath filePath = textDocument->filePath();
226 const QString &previousResultId = m_tokens.value(filePath).resultId().value_or(QString());
227 if (!previousResultId.isEmpty()) {
228 SemanticTokensDeltaParams params;
229 params.setTextDocument(TextDocumentIdentifier(DocumentUri::fromFilePath(filePath)));
230 params.setPreviousResultId(previousResultId);
231 SemanticTokensFullDeltaRequest request(params);
232 request.setResponseCallback(
233 [this, filePath](const SemanticTokensFullDeltaRequest::Response &response) {
234 handleSemanticTokensDelta(filePath, response.result().value_or(nullptr));
235 });
236 m_client->sendContent(request);
237 return;
238 }
239 }
240 reloadSemanticTokens(textDocument);
241 }
242
rehighlight()243 void SemanticTokenSupport::rehighlight()
244 {
245 for (const Utils::FilePath &filePath : m_tokens.keys())
246 highlight(filePath);
247 }
248
addModifiers(int key,QHash<int,QTextCharFormat> * formatHash,TextStyles styles,QList<int> tokenModifiers,const TextEditor::FontSettings & fs)249 void addModifiers(int key,
250 QHash<int, QTextCharFormat> *formatHash,
251 TextStyles styles,
252 QList<int> tokenModifiers,
253 const TextEditor::FontSettings &fs)
254 {
255 if (tokenModifiers.isEmpty())
256 return;
257 int modifier = tokenModifiers.takeLast();
258 if (modifier < 0)
259 return;
260 auto addModifier = [&](TextStyle style) {
261 if (key & modifier) // already there don't add twice
262 return;
263 key = key | modifier;
264 styles.mixinStyles.push_back(style);
265 formatHash->insert(key, fs.toTextCharFormat(styles));
266 };
267 switch (modifier) {
268 case declarationModifier: addModifier(C_DECLARATION); break;
269 case definitionModifier: addModifier(C_FUNCTION_DEFINITION); break;
270 default: break;
271 }
272 addModifiers(key, formatHash, styles, tokenModifiers, fs);
273 }
274
setLegend(const LanguageServerProtocol::SemanticTokensLegend & legend)275 void SemanticTokenSupport::setLegend(const LanguageServerProtocol::SemanticTokensLegend &legend)
276 {
277 m_tokenTypeStrings = legend.tokenTypes();
278 m_tokenModifierStrings = legend.tokenModifiers();
279 m_tokenTypes = Utils::transform(legend.tokenTypes(), [&](const QString &tokenTypeString){
280 return m_tokenTypesMap.value(tokenTypeString, -1);
281 });
282 m_tokenModifiers = Utils::transform(legend.tokenModifiers(), [&](const QString &tokenModifierString){
283 return m_tokenModifiersMap.value(tokenModifierString, -1);
284 });
285 updateFormatHash();
286 }
287
updateFormatHash()288 void SemanticTokenSupport::updateFormatHash()
289 {
290 auto fontSettings = TextEditorSettings::fontSettings();
291 for (int tokenType : qAsConst(m_tokenTypes)) {
292 if (tokenType < 0)
293 continue;
294 TextStyle style;
295 switch (tokenType) {
296 case typeToken: style = C_TYPE; break;
297 case classToken: style = C_TYPE; break;
298 case enumMemberToken: style = C_ENUMERATION; break;
299 case typeParameterToken: style = C_FIELD; break;
300 case parameterToken: style = C_PARAMETER; break;
301 case variableToken: style = C_LOCAL; break;
302 case functionToken: style = C_FUNCTION; break;
303 case methodToken: style = C_FUNCTION; break;
304 case macroToken: style = C_PREPROCESSOR; break;
305 case keywordToken: style = C_KEYWORD; break;
306 case commentToken: style = C_COMMENT; break;
307 case stringToken: style = C_STRING; break;
308 case numberToken: style = C_NUMBER; break;
309 case operatorToken: style = C_OPERATOR; break;
310 default:
311 style = m_additionalTypeStyles.value(tokenType, C_TEXT);
312 break;
313 }
314 int mainHashPart = tokenType << tokenTypeBitOffset;
315 m_formatHash[mainHashPart] = fontSettings.toTextCharFormat(style);
316 TextStyles styles;
317 styles.mainStyle = style;
318 styles.mixinStyles.initializeElements();
319 addModifiers(mainHashPart, &m_formatHash, styles, m_tokenModifiers, fontSettings);
320 }
321 rehighlight();
322 }
323
setTokenTypesMap(const QMap<QString,int> & tokenTypesMap)324 void SemanticTokenSupport::setTokenTypesMap(const QMap<QString, int> &tokenTypesMap)
325 {
326 m_tokenTypesMap = tokenTypesMap;
327 }
328
setTokenModifiersMap(const QMap<QString,int> & tokenModifiersMap)329 void SemanticTokenSupport::setTokenModifiersMap(const QMap<QString, int> &tokenModifiersMap)
330 {
331 m_tokenModifiersMap = tokenModifiersMap;
332 }
333
setAdditionalTokenTypeStyles(const QHash<int,TextStyle> & typeStyles)334 void SemanticTokenSupport::setAdditionalTokenTypeStyles(
335 const QHash<int, TextStyle> &typeStyles)
336 {
337 m_additionalTypeStyles = typeStyles;
338 }
339
340 //void SemanticTokenSupport::setAdditionalTokenModifierStyles(
341 // const QHash<int, TextStyle> &modifierStyles)
342 //{
343 // m_additionalModifierStyles = modifierStyles;
344 //}
345
supportedSemanticRequests(TextDocument * document) const346 SemanticRequestTypes SemanticTokenSupport::supportedSemanticRequests(TextDocument *document) const
347 {
348 auto supportedRequests = [&](const QJsonObject &options) -> SemanticRequestTypes {
349 TextDocumentRegistrationOptions docOptions(options);
350 if (docOptions.isValid()
351 && docOptions.filterApplies(document->filePath(),
352 Utils::mimeTypeForName(document->mimeType()))) {
353 return SemanticRequestType::None;
354 }
355 const SemanticTokensOptions semanticOptions(options);
356 return semanticOptions.supportedRequests();
357 };
358 const QString dynamicMethod = "textDocument/semanticTokens";
359 const DynamicCapabilities &dynamicCapabilities = m_client->dynamicCapabilities();
360 if (auto registered = dynamicCapabilities.isRegistered(dynamicMethod);
361 registered.has_value()) {
362 if (!registered.value())
363 return SemanticRequestType::None;
364 return supportedRequests(dynamicCapabilities.option(dynamicMethod).toObject());
365 }
366 if (m_client->capabilities().semanticTokensProvider().has_value())
367 return supportedRequests(m_client->capabilities().semanticTokensProvider().value());
368 return SemanticRequestType::None;
369 }
370
handleSemanticTokens(const Utils::FilePath & filePath,const SemanticTokensResult & result)371 void SemanticTokenSupport::handleSemanticTokens(const Utils::FilePath &filePath,
372 const SemanticTokensResult &result)
373 {
374 if (auto tokens = Utils::get_if<SemanticTokens>(&result)) {
375 m_tokens[filePath] = *tokens;
376 highlight(filePath);
377 } else {
378 m_tokens.remove(filePath);
379 }
380 }
381
handleSemanticTokensDelta(const Utils::FilePath & filePath,const LanguageServerProtocol::SemanticTokensDeltaResult & result)382 void SemanticTokenSupport::handleSemanticTokensDelta(
383 const Utils::FilePath &filePath, const LanguageServerProtocol::SemanticTokensDeltaResult &result)
384 {
385 if (auto tokens = Utils::get_if<SemanticTokens>(&result)) {
386 m_tokens[filePath] = *tokens;
387 } else if (auto tokensDelta = Utils::get_if<SemanticTokensDelta>(&result)) {
388 QList<SemanticTokensEdit> edits = tokensDelta->edits();
389 if (edits.isEmpty())
390 return;
391
392 Utils::sort(edits, &SemanticTokensEdit::start);
393
394 SemanticTokens &tokens = m_tokens[filePath];
395 const QList<int> &data = tokens.data();
396
397 int newDataSize = data.size();
398 for (const SemanticTokensEdit &edit : qAsConst(edits))
399 newDataSize += edit.dataSize() - edit.deleteCount();
400 QList<int> newData;
401 newData.reserve(newDataSize);
402
403 auto it = data.begin();
404 const auto end = data.end();
405 for (const SemanticTokensEdit &edit : qAsConst(edits)) {
406 if (edit.start() > data.size()) // prevent edits after the previously reported data
407 return;
408 for (const auto start = data.begin() + edit.start(); it < start; ++it)
409 newData.append(*it);
410 newData.append(edit.data().value_or(QList<int>()));
411 int deleteCount = edit.deleteCount();
412 if (deleteCount > std::distance(it, end)) {
413 qCDebug(LOGLSPHIGHLIGHT)
414 << "We shall delete more highlight data entries than we actually have, "
415 "so we are out of sync with the server. "
416 "Request full semantic tokens again.";
417 TextDocument *doc = TextDocument::textDocumentForFilePath(filePath);
418 if (doc && LanguageClientManager::clientForDocument(doc) == m_client)
419 reloadSemanticTokens(doc);
420 return;
421 }
422 it += deleteCount;
423 }
424 for (; it != end; ++it)
425 newData.append(*it);
426
427 tokens.setData(newData);
428 tokens.setResultId(tokensDelta->resultId());
429 } else {
430 m_tokens.remove(filePath);
431 return;
432 }
433 highlight(filePath);
434 }
435
highlight(const Utils::FilePath & filePath)436 void SemanticTokenSupport::highlight(const Utils::FilePath &filePath)
437 {
438 TextDocument *doc = TextDocument::textDocumentForFilePath(filePath);
439 if (!doc || LanguageClientManager::clientForDocument(doc) != m_client)
440 return;
441 SyntaxHighlighter *highlighter = doc->syntaxHighlighter();
442 if (!highlighter)
443 return;
444 const QList<SemanticToken> tokens = m_tokens.value(filePath).toTokens(m_tokenTypes,
445 m_tokenModifiers);
446 if (m_tokensHandler) {
447 int line = 1;
448 int column = 1;
449 QList<ExpandedSemanticToken> expandedTokens;
450 for (const SemanticToken &token : tokens) {
451 line += token.deltaLine;
452 if (token.deltaLine != 0) // reset the current column when we change the current line
453 column = 1;
454 column += token.deltaStart;
455 if (token.tokenIndex >= m_tokenTypeStrings.length())
456 continue;
457 ExpandedSemanticToken expandedToken;
458 expandedToken.type = m_tokenTypeStrings.at(token.tokenIndex);
459 int modifiers = token.rawTokenModifiers;
460 for (int bitPos = 0; modifiers && bitPos < m_tokenModifierStrings.length();
461 ++bitPos, modifiers >>= 1) {
462 if (modifiers & 0x1)
463 expandedToken.modifiers << m_tokenModifierStrings.at(bitPos);
464 }
465 expandedToken.line = line;
466 expandedToken.column = column;
467 expandedToken.length = token.length;
468 expandedTokens << expandedToken;
469 };
470 m_tokensHandler(doc, expandedTokens);
471 return;
472 }
473 int line = 1;
474 int column = 1;
475 auto toResult = [&](const SemanticToken &token){
476 line += token.deltaLine;
477 if (token.deltaLine != 0) // reset the current column when we change the current line
478 column = 1;
479 column += token.deltaStart;
480 const int tokenKind = token.tokenType << tokenTypeBitOffset | token.tokenModifiers;
481 return HighlightingResult(line, column, token.length, tokenKind);
482 };
483 const HighlightingResults results = Utils::transform(tokens, toResult);
484 SemanticHighlighter::setExtraAdditionalFormats(highlighter, results, m_formatHash);
485 }
486
487 } // namespace LanguageClient
488