blob: b296b9905a5762b3f4053da475832de2ce5de1ec [file] [log] [blame]
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/src/computer/computer_highlights.dart';
import 'package:analysis_server/src/lsp/constants.dart';
import 'package:analysis_server/src/lsp/error_or.dart';
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
import 'package:analysis_server/src/lsp/mapping.dart';
import 'package:analysis_server/src/lsp/registration/feature_registration.dart';
import 'package:analysis_server/src/lsp/semantic_tokens/encoder.dart';
import 'package:analysis_server/src/lsp/semantic_tokens/legend.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/source/source_range.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart';
import 'package:collection/collection.dart';
typedef StaticOptions =
Either2<SemanticTokensOptions, SemanticTokensRegistrationOptions>;
abstract class AbstractSemanticTokensHandler<T>
extends LspMessageHandler<T, SemanticTokens?>
with LspPluginRequestHandlerMixin {
AbstractSemanticTokensHandler(super.server);
List<List<HighlightRegion>> getPluginResults(String path) {
var notificationManager = server.notificationManager;
return notificationManager.highlights.getResults(path);
}
Future<List<SemanticTokenInfo>> getServerResult(
CompilationUnit unit,
SourceRange? range,
) async {
var computer = DartUnitHighlightsComputer(unit, range: range);
return computer.computeSemanticTokens();
}
Iterable<SemanticTokenInfo> _filter(
Iterable<SemanticTokenInfo> tokens,
SourceRange? range,
) {
if (range == null) {
return tokens;
}
return tokens.where(
(token) =>
!(token.offset + token.length < range.offset ||
token.offset > range.end),
);
}
Future<ErrorOr<SemanticTokens?>> _handleImpl(
TextDocumentIdentifier textDocument,
CancellationToken token, {
Range? range,
}) {
var path = pathOfDoc(textDocument);
return path.mapResult((path) async {
// Always prefer a LineInfo from a resolved unit than server.getLineInfo.
var resolvedUnit = (await requireResolvedUnit(path)).resultOrNull;
var lineInfo = resolvedUnit?.lineInfo ?? server.getLineInfo(path);
// If there is no lineInfo, the request cannot be translated from LSP
// line/col to server offset/length.
if (lineInfo == null) {
return success(null);
}
return toSourceRangeNullable(lineInfo, range).mapResult((range) async {
var serverTokens =
resolvedUnit != null
? await getServerResult(resolvedUnit.unit, range)
: <SemanticTokenInfo>[];
var pluginHighlightRegions = getPluginResults(path).flattenedToList;
if (token.isCancellationRequested) {
return cancelled(token);
}
var encoder = SemanticTokenEncoder();
Iterable<SemanticTokenInfo> pluginTokens = encoder
.convertHighlightToTokens(pluginHighlightRegions);
// Plugin tokens are not filtered at source, so need to be filtered here.
pluginTokens = _filter(pluginTokens, range);
Iterable<SemanticTokenInfo> tokens = [...serverTokens, ...pluginTokens];
// Capabilities exist for supporting multiline/overlapping tokens. These
// could be used if any clients take it up (VS Code does not).
// - clientCapabilities?.multilineTokenSupport
// - clientCapabilities?.overlappingTokenSupport
var allowMultilineTokens = false;
var allowOverlappingTokens = false;
// Some of the translation operations and the final encoding require
// the tokens to be sorted. Do it once here to avoid each method needing
// to do it itself (resulting in multiple sorts).
tokens =
tokens.toList()..sort(SemanticTokenInfo.offsetLengthPrioritySort);
if (!allowOverlappingTokens) {
tokens = encoder.splitOverlappingTokens(tokens);
}
if (!allowMultilineTokens) {
tokens = tokens.expand(
(token) => encoder.splitMultilineTokens(token, lineInfo),
);
// Tokens may need re-filtering after being split up as there may
// now be tokens outside of the range.
tokens = _filter(tokens, range);
}
var semanticTokens = encoder.encodeTokens(tokens.toList(), lineInfo);
return success(semanticTokens);
});
});
}
}
class SemanticTokensFullHandler
extends AbstractSemanticTokensHandler<SemanticTokensParams> {
SemanticTokensFullHandler(super.server);
@override
Method get handlesMessage => Method.textDocument_semanticTokens_full;
@override
LspJsonHandler<SemanticTokensParams> get jsonHandler =>
SemanticTokensParams.jsonHandler;
@override
Future<ErrorOr<SemanticTokens?>> handle(
SemanticTokensParams params,
MessageInfo message,
CancellationToken token,
) => _handleImpl(params.textDocument, token);
}
class SemanticTokensRangeHandler
extends AbstractSemanticTokensHandler<SemanticTokensRangeParams> {
SemanticTokensRangeHandler(super.server);
@override
Method get handlesMessage => Method.textDocument_semanticTokens_range;
@override
LspJsonHandler<SemanticTokensRangeParams> get jsonHandler =>
SemanticTokensRangeParams.jsonHandler;
@override
Future<ErrorOr<SemanticTokens?>> handle(
SemanticTokensRangeParams params,
MessageInfo message,
CancellationToken token,
) => _handleImpl(params.textDocument, token, range: params.range);
}
class SemanticTokensRegistrations extends FeatureRegistration
with SingleDynamicRegistration, StaticRegistration<StaticOptions> {
SemanticTokensRegistrations(super.info);
@override
ToJsonable? get options => SemanticTokensRegistrationOptions(
documentSelector: fullySupportedTypes,
legend: semanticTokenLegend.lspLegend,
full: Either2<bool, SemanticTokensFullDelta>.t2(
SemanticTokensFullDelta(delta: false),
),
range: Either2<bool, SemanticTokensOptionsRange>.t1(true),
);
@override
Method get registrationMethod =>
CustomMethods.semanticTokenDynamicRegistration;
@override
StaticOptions get staticOptions => Either2.t1(
SemanticTokensOptions(
legend: semanticTokenLegend.lspLegend,
full: Either2.t2(SemanticTokensFullDelta(delta: false)),
range: Either2.t1(true),
),
);
@override
bool get supportsDynamic => clientDynamic.semanticTokens;
}