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