blob: 0b28c2c9a07d173a24b11a69c1bc3b35f285e27d [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 'package:analysis_server/lsp_protocol/protocol_generated.dart';
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
import 'package:analysis_server/src/lsp/client_capabilities.dart';
import 'package:analysis_server/src/lsp/constants.dart';
import 'package:analysis_server/src/lsp/lsp_analysis_server.dart';
import 'package:analysis_server/src/lsp/semantic_tokens/legend.dart';
/// Helper for reading client dynamic registrations which may be ommitted by the
/// client.
class ClientDynamicRegistrations {
/// All dynamic registrations supported by the Dart LSP server.
///
/// Anything listed here and supported by the client will not send a static
/// registration but instead dynamically register (usually only for a subset of
/// files such as for .dart/pubspec.yaml/etc).
///
/// When adding new capabilities that will be registered dynamically, the
/// test_dynamicRegistration_XXX tests in `lsp/initialization_test.dart` should
/// also be updated to ensure no double-registrations.
static const supported = [
Method.textDocument_didOpen,
Method.textDocument_didChange,
Method.textDocument_didClose,
Method.textDocument_completion,
Method.textDocument_hover,
Method.textDocument_signatureHelp,
Method.textDocument_references,
Method.textDocument_documentHighlight,
Method.textDocument_documentColor,
Method.textDocument_formatting,
Method.textDocument_onTypeFormatting,
Method.textDocument_rangeFormatting,
Method.textDocument_definition,
Method.textDocument_codeAction,
Method.textDocument_rename,
Method.textDocument_foldingRange,
Method.textDocument_selectionRange,
// workspace.fileOperations covers all file operation methods but we only
// support this one.
Method.workspace_willRenameFiles,
// Sematic tokens are all registered under a single "method" as the
// actual methods are controlled by the server capabilities.
CustomMethods.semanticTokenDynamicRegistration,
];
final ClientCapabilities _capabilities;
ClientDynamicRegistrations(this._capabilities);
bool get codeActions =>
_capabilities.textDocument?.foldingRange?.dynamicRegistration ?? false;
bool get colorProvider =>
_capabilities.textDocument?.colorProvider?.dynamicRegistration ?? false;
bool get completion =>
_capabilities.textDocument?.completion?.dynamicRegistration ?? false;
bool get definition =>
_capabilities.textDocument?.definition?.dynamicRegistration ?? false;
bool get didChangeConfiguration =>
_capabilities.workspace?.didChangeConfiguration?.dynamicRegistration ??
false;
bool get documentHighlights =>
_capabilities.textDocument?.documentHighlight?.dynamicRegistration ??
false;
bool get documentSymbol =>
_capabilities.textDocument?.documentSymbol?.dynamicRegistration ?? false;
bool get fileOperations =>
_capabilities.workspace?.fileOperations?.dynamicRegistration ?? false;
bool get folding =>
_capabilities.textDocument?.foldingRange?.dynamicRegistration ?? false;
bool get formatting =>
_capabilities.textDocument?.formatting?.dynamicRegistration ?? false;
bool get hover =>
_capabilities.textDocument?.hover?.dynamicRegistration ?? false;
bool get implementation =>
_capabilities.textDocument?.implementation?.dynamicRegistration ?? false;
bool get rangeFormatting =>
_capabilities.textDocument?.rangeFormatting?.dynamicRegistration ?? false;
bool get references =>
_capabilities.textDocument?.references?.dynamicRegistration ?? false;
bool get rename =>
_capabilities.textDocument?.rename?.dynamicRegistration ?? false;
bool get selectionRange =>
_capabilities.textDocument?.selectionRange?.dynamicRegistration ?? false;
bool get semanticTokens =>
_capabilities.textDocument?.semanticTokens?.dynamicRegistration ?? false;
bool get signatureHelp =>
_capabilities.textDocument?.signatureHelp?.dynamicRegistration ?? false;
bool get textSync =>
_capabilities.textDocument?.synchronization?.dynamicRegistration ?? false;
bool get typeFormatting =>
_capabilities.textDocument?.onTypeFormatting?.dynamicRegistration ??
false;
}
class ServerCapabilitiesComputer {
static final fileOperationRegistrationOptions =
FileOperationRegistrationOptions(
filters: [
FileOperationFilter(
scheme: 'file',
pattern: FileOperationPattern(
glob: '**/*.dart',
matches: FileOperationPatternKind.file,
),
)
],
);
final LspAnalysisServer _server;
/// List of current registrations.
Set<Registration> currentRegistrations = {};
var _lastRegistrationId = 0;
final dartFiles = DocumentFilter(language: 'dart', scheme: 'file');
final pubspecFile = DocumentFilter(
language: 'yaml', scheme: 'file', pattern: '**/pubspec.yaml');
final analysisOptionsFile = DocumentFilter(
language: 'yaml', scheme: 'file', pattern: '**/analysis_options.yaml');
final fixDataFile = DocumentFilter(
language: 'yaml', scheme: 'file', pattern: '**/lib/fix_data.yaml');
ServerCapabilitiesComputer(this._server);
ServerCapabilities computeServerCapabilities(
LspClientCapabilities clientCapabilities) {
final codeActionLiteralSupport = clientCapabilities.literalCodeActions;
final renameOptionsSupport = clientCapabilities.renameValidation;
final enableFormatter =
_server.clientConfiguration.global.enableSdkFormatter;
final previewCommitCharacters =
_server.clientConfiguration.global.previewCommitCharacters;
final dynamicRegistrations =
ClientDynamicRegistrations(clientCapabilities.raw);
// When adding new capabilities to the server that may apply to specific file
// types, it's important to update
// [IntializedMessageHandler._performDynamicRegistration()] to notify
// supporting clients of this. This avoids clients needing to hard-code the
// list of what files types we support (and allows them to avoid sending
// requests where we have only partial support for some types).
return ServerCapabilities(
textDocumentSync: dynamicRegistrations.textSync
? null
: Either2<TextDocumentSyncOptions, TextDocumentSyncKind>.t1(
TextDocumentSyncOptions(
// The open/close and sync kind flags are registered dynamically if the
// client supports them, so these static registrations are based on whether
// the client supports dynamic registration.
openClose: true,
change: TextDocumentSyncKind.Incremental,
willSave: false,
willSaveWaitUntil: false,
save: null,
)),
completionProvider: dynamicRegistrations.completion
? null
: CompletionOptions(
triggerCharacters: dartCompletionTriggerCharacters,
allCommitCharacters: previewCommitCharacters
? dartCompletionCommitCharacters
: null,
resolveProvider: true,
),
hoverProvider: dynamicRegistrations.hover
? null
: Either2<bool, HoverOptions>.t1(true),
signatureHelpProvider: dynamicRegistrations.signatureHelp
? null
: SignatureHelpOptions(
triggerCharacters: dartSignatureHelpTriggerCharacters,
retriggerCharacters: dartSignatureHelpRetriggerCharacters,
),
definitionProvider: dynamicRegistrations.definition
? null
: Either2<bool, DefinitionOptions>.t1(true),
implementationProvider: dynamicRegistrations.implementation
? null
: Either3<bool, ImplementationOptions,
ImplementationRegistrationOptions>.t1(
true,
),
referencesProvider: dynamicRegistrations.references
? null
: Either2<bool, ReferenceOptions>.t1(true),
documentHighlightProvider: dynamicRegistrations.documentHighlights
? null
: Either2<bool, DocumentHighlightOptions>.t1(true),
documentSymbolProvider: dynamicRegistrations.documentSymbol
? null
: Either2<bool, DocumentSymbolOptions>.t1(true),
// "The `CodeActionOptions` return type is only valid if the client
// signals code action literal support via the property
// `textDocument.codeAction.codeActionLiteralSupport`."
codeActionProvider: dynamicRegistrations.codeActions
? null
: codeActionLiteralSupport
? Either2<bool, CodeActionOptions>.t2(CodeActionOptions(
codeActionKinds: DartCodeActionKind.serverSupportedKinds,
))
: Either2<bool, CodeActionOptions>.t1(true),
colorProvider: dynamicRegistrations.colorProvider
? null
: Either3<bool, DocumentColorOptions,
DocumentColorRegistrationOptions>.t3(
DocumentColorRegistrationOptions(documentSelector: [dartFiles])),
documentFormattingProvider: dynamicRegistrations.formatting
? null
: Either2<bool, DocumentFormattingOptions>.t1(enableFormatter),
documentOnTypeFormattingProvider: dynamicRegistrations.typeFormatting
? null
: enableFormatter
? DocumentOnTypeFormattingOptions(
firstTriggerCharacter: dartTypeFormattingCharacters.first,
moreTriggerCharacter:
dartTypeFormattingCharacters.skip(1).toList())
: null,
documentRangeFormattingProvider: dynamicRegistrations.typeFormatting
? null
: Either2<bool, DocumentRangeFormattingOptions>.t1(enableFormatter),
renameProvider: dynamicRegistrations.rename
? null
: renameOptionsSupport
? Either2<bool, RenameOptions>.t2(
RenameOptions(prepareProvider: true))
: Either2<bool, RenameOptions>.t1(true),
foldingRangeProvider: dynamicRegistrations.folding
? null
: Either3<bool, FoldingRangeOptions,
FoldingRangeRegistrationOptions>.t1(
true,
),
selectionRangeProvider: dynamicRegistrations.selectionRange
? null
: Either3<bool, SelectionRangeOptions,
SelectionRangeRegistrationOptions>.t1(true),
semanticTokensProvider: dynamicRegistrations.semanticTokens
? null
: Either2<SemanticTokensOptions,
SemanticTokensRegistrationOptions>.t1(
SemanticTokensOptions(
legend: semanticTokenLegend.lspLegend,
full: Either2<bool, SemanticTokensOptionsFull>.t2(
SemanticTokensOptionsFull(delta: false),
),
range: Either2<bool, SemanticTokensOptionsRange>.t1(true),
),
),
executeCommandProvider: ExecuteCommandOptions(
commands: Commands.serverSupportedCommands,
workDoneProgress: true,
),
workspaceSymbolProvider: Either2<bool, WorkspaceSymbolOptions>.t1(true),
workspace: ServerCapabilitiesWorkspace(
workspaceFolders: WorkspaceFoldersServerCapabilities(
supported: true,
changeNotifications: Either2<String, bool>.t2(true),
),
fileOperations: dynamicRegistrations.fileOperations
? null
: ServerCapabilitiesFileOperations(
willRename: fileOperationRegistrationOptions,
),
),
);
}
/// If the client supports dynamic registrations we can tell it what methods
/// we support for which documents. For example, this allows us to ask for
/// file edits for .dart as well as pubspec.yaml but only get hover/completion
/// calls for .dart. This functionality may not be supported by the client, in
/// which case they will use the ServerCapabilities to know which methods we
/// support and it will be up to them to decide which file types they will
/// send requests for.
Future<void> performDynamicRegistration() async {
final pluginTypes = _server.pluginManager.plugins
.expand((plugin) => plugin.currentSession?.interestingFiles ?? const [])
// All published plugins use something like `*.extension` as
// interestingFiles. Prefix a `**/` so that the glob matches nested
// folders as well.
.map((glob) => DocumentFilter(scheme: 'file', pattern: '**/$glob'));
final pluginTypesExcludingDart =
pluginTypes.where((filter) => filter.pattern != '**/*.dart');
final fullySupportedTypes = {dartFiles, ...pluginTypes}.toList();
// Add pubspec + analysis options only for synchronisation. We do not support
// things like hovers/formatting/etc. for these files so there's no point
// in having the client send those requests (plus, for things like formatting
// this could result in the editor reporting "multiple formatters installed"
// and prevent a built-in YAML formatter from being selected).
final synchronisedTypes = {
...fullySupportedTypes,
pubspecFile,
analysisOptionsFile,
fixDataFile,
}.toList();
// Completion is supported for some synchronised files that we don't _fully_
// support (eg. YAML). If these gain support for things like hover, we may
// wish to move them to fullySupprtedTypes but add an exclusion for formatting.
final completionSupportedTypesExcludingDart = {
// Dart is excluded here at it's registered separately with trigger/commit
// characters.
...pluginTypesExcludingDart,
pubspecFile,
analysisOptionsFile,
fixDataFile,
}.toList();
final registrations = <Registration>[];
final enableFormatter =
_server.clientConfiguration.global.enableSdkFormatter;
final previewCommitCharacters =
_server.clientConfiguration.global.previewCommitCharacters;
/// Helper for creating registrations with IDs.
void register(bool condition, Method method, [ToJsonable? options]) {
if (condition == true) {
registrations.add(Registration(
id: (_lastRegistrationId++).toString(),
method: method.toString(),
registerOptions: options));
}
}
final dynamicRegistrations =
ClientDynamicRegistrations(_server.clientCapabilities!.raw);
register(
dynamicRegistrations.textSync,
Method.textDocument_didOpen,
TextDocumentRegistrationOptions(documentSelector: synchronisedTypes),
);
register(
dynamicRegistrations.textSync,
Method.textDocument_didClose,
TextDocumentRegistrationOptions(documentSelector: synchronisedTypes),
);
register(
dynamicRegistrations.textSync,
Method.textDocument_didChange,
TextDocumentChangeRegistrationOptions(
syncKind: TextDocumentSyncKind.Incremental,
documentSelector: synchronisedTypes),
);
// Trigger and commit characters are specific to Dart, so register them
// separately to the others.
register(
dynamicRegistrations.completion,
Method.textDocument_completion,
CompletionRegistrationOptions(
documentSelector: [dartFiles],
triggerCharacters: dartCompletionTriggerCharacters,
allCommitCharacters:
previewCommitCharacters ? dartCompletionCommitCharacters : null,
resolveProvider: true,
),
);
register(
dynamicRegistrations.completion,
Method.textDocument_completion,
CompletionRegistrationOptions(
documentSelector: completionSupportedTypesExcludingDart,
resolveProvider: true,
),
);
register(
dynamicRegistrations.hover,
Method.textDocument_hover,
TextDocumentRegistrationOptions(documentSelector: fullySupportedTypes),
);
register(
dynamicRegistrations.signatureHelp,
Method.textDocument_signatureHelp,
SignatureHelpRegistrationOptions(
documentSelector: fullySupportedTypes,
triggerCharacters: dartSignatureHelpTriggerCharacters,
retriggerCharacters: dartSignatureHelpRetriggerCharacters,
),
);
register(
dynamicRegistrations.references,
Method.textDocument_references,
TextDocumentRegistrationOptions(documentSelector: fullySupportedTypes),
);
register(
dynamicRegistrations.documentHighlights,
Method.textDocument_documentHighlight,
TextDocumentRegistrationOptions(documentSelector: fullySupportedTypes),
);
register(
dynamicRegistrations.documentSymbol,
Method.textDocument_documentSymbol,
TextDocumentRegistrationOptions(documentSelector: fullySupportedTypes),
);
register(
dynamicRegistrations.colorProvider,
// This registration covers both documentColor and colorPresentation.
Method.textDocument_documentColor,
DocumentColorRegistrationOptions(documentSelector: [dartFiles]),
);
register(
enableFormatter && dynamicRegistrations.formatting,
Method.textDocument_formatting,
TextDocumentRegistrationOptions(documentSelector: fullySupportedTypes),
);
register(
enableFormatter && dynamicRegistrations.typeFormatting,
Method.textDocument_onTypeFormatting,
DocumentOnTypeFormattingRegistrationOptions(
documentSelector: [dartFiles], // This one is currently Dart-specific
firstTriggerCharacter: dartTypeFormattingCharacters.first,
moreTriggerCharacter: dartTypeFormattingCharacters.skip(1).toList(),
),
);
register(
enableFormatter && dynamicRegistrations.rangeFormatting,
Method.textDocument_rangeFormatting,
DocumentRangeFormattingRegistrationOptions(
documentSelector: [dartFiles], // This one is currently Dart-specific
),
);
register(
dynamicRegistrations.definition,
Method.textDocument_definition,
TextDocumentRegistrationOptions(documentSelector: fullySupportedTypes),
);
register(
dynamicRegistrations.implementation,
Method.textDocument_implementation,
TextDocumentRegistrationOptions(documentSelector: fullySupportedTypes),
);
register(
dynamicRegistrations.codeActions,
Method.textDocument_codeAction,
CodeActionRegistrationOptions(
documentSelector: fullySupportedTypes,
codeActionKinds: DartCodeActionKind.serverSupportedKinds,
),
);
register(
dynamicRegistrations.rename,
Method.textDocument_rename,
RenameRegistrationOptions(
documentSelector: fullySupportedTypes, prepareProvider: true),
);
register(
dynamicRegistrations.folding,
Method.textDocument_foldingRange,
TextDocumentRegistrationOptions(documentSelector: fullySupportedTypes),
);
register(
dynamicRegistrations.fileOperations,
Method.workspace_willRenameFiles,
fileOperationRegistrationOptions,
);
register(
dynamicRegistrations.didChangeConfiguration,
Method.workspace_didChangeConfiguration,
);
register(
dynamicRegistrations.selectionRange,
Method.textDocument_selectionRange,
SelectionRangeRegistrationOptions(
documentSelector: [dartFiles],
),
);
register(
dynamicRegistrations.semanticTokens,
CustomMethods.semanticTokenDynamicRegistration,
SemanticTokensRegistrationOptions(
documentSelector: fullySupportedTypes,
legend: semanticTokenLegend.lspLegend,
full: Either2<bool, SemanticTokensOptionsFull>.t2(
SemanticTokensOptionsFull(delta: false),
),
range: Either2<bool, SemanticTokensOptionsRange>.t1(true),
),
);
await _applyRegistrations(registrations);
}
Future<void> _applyRegistrations(List<Registration> newRegistrations) async {
// Compute a diff of old and new registrations to send the unregister or
// another register request. We compare registrations by their methods and
// the hashcode of their registration options to allow for multiple
// registrations of a single method.
String _registrationHash(Registration registration) =>
'${registration.method}${registration.registerOptions.hashCode}';
final newRegistrationsMap = Map.fromEntries(
newRegistrations.map((r) => MapEntry(r, _registrationHash(r))));
final newRegistrationsJsons = newRegistrationsMap.values.toSet();
final currentRegistrationsMap = Map.fromEntries(
currentRegistrations.map((r) => MapEntry(r, _registrationHash(r))));
final currentRegistrationJsons = currentRegistrationsMap.values.toSet();
final registrationsToAdd = newRegistrationsMap.entries
.where((entry) => !currentRegistrationJsons.contains(entry.value))
.map((entry) => entry.key)
.toList();
final registrationsToRemove = currentRegistrationsMap.entries
.where((entry) => !newRegistrationsJsons.contains(entry.value))
.map((entry) => entry.key)
.toList();
// Update the current list before we start sending requests since we
// go async.
currentRegistrations
..removeAll(registrationsToRemove)
..addAll(registrationsToAdd);
Future<void>? unregistrationRequest;
if (registrationsToRemove.isNotEmpty) {
final unregistrations = registrationsToRemove
.map((r) => Unregistration(id: r.id, method: r.method))
.toList();
// It's important not to await this request here, as we must ensure
// we cannot re-enter this method until we have sent both the unregister
// and register requests to the client atomically.
// https://github.com/dart-lang/sdk/issues/47851#issuecomment-988093109
unregistrationRequest = _server.sendRequest(
Method.client_unregisterCapability,
UnregistrationParams(unregisterations: unregistrations));
}
Future<void>? registrationRequest;
// Only send the registration request if we have at least one (since
// otherwise we don't know that the client supports registerCapability).
if (registrationsToAdd.isNotEmpty) {
registrationRequest = _server
.sendRequest(Method.client_registerCapability,
RegistrationParams(registrations: registrationsToAdd))
.then((registrationResponse) {
final error = registrationResponse.error;
if (error != null) {
_server.logErrorToClient(
'Failed to register capabilities with client: '
'(${error.code}) '
'${error.message}',
);
}
});
}
// Only after we have sent both unregistration + registration events may
// we await them, knowing another "thread" could not have executed this
// method between them.
await unregistrationRequest;
await registrationRequest;
}
}