Support plugin notifications in LSP server
This adds support for analysis domains that plugins contribute to by
sending notifications. I've only implemented folding so far, but most
of this CL is to cache results from plugins and to adapt sending server
capabilities.
Instead of using a NullNotificationManager, the LSP server now uses a
regular NotificationManager, except that results won't be forwarded to
clients. LSP handlers will fetch partial results from plugins and merge
them with data from the server that will be computed when needed.
I've extracted the server capabilities calculation from the init
handlers into ServerCapabilityComputer. It will also contain the
interestingFiles glob from active plugins and re-register capabilities
whenever plugins change.
Change-Id: I9869240cbfa284592e952498933e638b89a2a763
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/142981
Reviewed-by: Danny Tuppeny <danny@tuppeny.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_folding.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_folding.dart
index bb721de..418e84d 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_folding.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_folding.dart
@@ -10,6 +10,9 @@
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
import 'package:analysis_server/src/lsp/lsp_analysis_server.dart';
import 'package:analysis_server/src/lsp/mapping.dart';
+import 'package:analysis_server/src/protocol_server.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:analyzer/source/line_info.dart';
class FoldingHandler
extends MessageHandler<FoldingRangeParams, List<FoldingRange>> {
@@ -24,16 +27,36 @@
@override
Future<ErrorOr<List<FoldingRange>>> handle(
FoldingRangeParams params, CancellationToken token) async {
- if (!isDartDocument(params.textDocument)) {
- return success(const []);
- }
-
final path = pathOfDoc(params.textDocument);
- final unit = await path.mapResult(requireUnresolvedUnit);
- return unit.mapResult((unit) {
- final lineInfo = unit.lineInfo;
- final regions = DartUnitFoldingComputer(lineInfo, unit.unit).compute();
+ return path.mapResult((path) async {
+ final partialResults = <List<FoldingRegion>>[];
+ LineInfo lineInfo;
+
+ final unit = server.getParsedUnit(path);
+ if (unit?.state == ResultState.VALID) {
+ lineInfo = unit.lineInfo;
+
+ final regions = DartUnitFoldingComputer(lineInfo, unit.unit).compute();
+ partialResults.insert(0, regions);
+ }
+
+ // Still try to obtain line info for invalid or non-Dart files, as plugins
+ // could contribute to those.
+ lineInfo ??= server.getLineInfo(path);
+
+ if (lineInfo == null) {
+ // Line information would be required to translate folding results to
+ // LSP.
+ return success(const []);
+ }
+
+ final notificationManager = server.notificationManager;
+ final pluginResults = notificationManager.folding.getResults(path);
+ partialResults.addAll(pluginResults);
+
+ final regions =
+ notificationManager.merger.mergeFoldingRegions(partialResults);
return success(
regions.map((region) => toFoldingRange(lineInfo, region)).toList(),
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_initialize.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_initialize.dart
index eeee66f..7df227a 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_initialize.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_initialize.dart
@@ -4,88 +4,10 @@
import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
-import 'package:analysis_server/src/lsp/constants.dart';
import 'package:analysis_server/src/lsp/handlers/handler_states.dart';
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
import 'package:analysis_server/src/lsp/lsp_analysis_server.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 intead 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_formatting,
- Method.textDocument_onTypeFormatting,
- Method.textDocument_definition,
- Method.textDocument_codeAction,
- Method.textDocument_rename,
- Method.textDocument_foldingRange,
- ];
- final ClientCapabilities _capabilities;
-
- ClientDynamicRegistrations(this._capabilities);
-
- bool get codeActions =>
- _capabilities.textDocument?.foldingRange?.dynamicRegistration ?? false;
-
- bool get completion =>
- _capabilities.textDocument?.completion?.dynamicRegistration ?? false;
-
- bool get definition =>
- _capabilities.textDocument?.definition?.dynamicRegistration ?? false;
-
- bool get documentHighlights =>
- _capabilities.textDocument?.documentHighlight?.dynamicRegistration ??
- false;
-
- bool get documentSymbol =>
- _capabilities.textDocument?.documentSymbol?.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 references =>
- _capabilities.textDocument?.references?.dynamicRegistration ?? false;
-
- bool get rename =>
- _capabilities.textDocument?.rename?.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 InitializeMessageHandler
extends MessageHandler<InitializeParams, InitializeResult> {
InitializeMessageHandler(LspAnalysisServer server) : super(server);
@@ -131,92 +53,8 @@
openWorkspacePaths,
);
- final codeActionLiteralSupport =
- params.capabilities.textDocument?.codeAction?.codeActionLiteralSupport;
-
- final renameOptionsSupport =
- params.capabilities.textDocument?.rename?.prepareSupport ?? false;
-
- final dynamicRegistrations =
- ClientDynamicRegistrations(params.capabilities);
-
- // 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).
- server.capabilities = ServerCapabilities(
- dynamicRegistrations.textSync
- ? null
- : Either2<TextDocumentSyncOptions, num>.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.
- true,
- TextDocumentSyncKind.Incremental,
- false,
- false,
- null,
- )),
- dynamicRegistrations.hover ? null : true, // hoverProvider
- dynamicRegistrations.completion
- ? null
- : CompletionOptions(
- true, // resolveProvider
- dartCompletionTriggerCharacters,
- ),
- dynamicRegistrations.signatureHelp
- ? null
- : SignatureHelpOptions(
- dartSignatureHelpTriggerCharacters,
- ),
- dynamicRegistrations.definition ? null : true, // definitionProvider
- null,
- dynamicRegistrations.implementation
- ? null
- : true, // implementationProvider
- dynamicRegistrations.references ? null : true, // referencesProvider
- dynamicRegistrations.documentHighlights
- ? null
- : true, // documentHighlightProvider
- dynamicRegistrations.documentSymbol
- ? null
- : true, // documentSymbolProvider
- true, // workspaceSymbolProvider
- // "The `CodeActionOptions` return type is only valid if the client
- // signals code action literal support via the property
- // `textDocument.codeAction.codeActionLiteralSupport`."
- dynamicRegistrations.codeActions
- ? null
- : codeActionLiteralSupport != null
- ? Either2<bool, CodeActionOptions>.t2(
- CodeActionOptions(DartCodeActionKind.serverSupportedKinds))
- : Either2<bool, CodeActionOptions>.t1(true),
- null,
- dynamicRegistrations.formatting
- ? null
- : true, // documentFormattingProvider
- false, // documentRangeFormattingProvider
- dynamicRegistrations.typeFormatting
- ? null
- : DocumentOnTypeFormattingOptions(
- dartTypeFormattingCharacters.first,
- dartTypeFormattingCharacters.skip(1).toList()),
- dynamicRegistrations.rename
- ? null
- : renameOptionsSupport
- ? Either2<bool, RenameOptions>.t2(RenameOptions(true))
- : Either2<bool, RenameOptions>.t1(true),
- null,
- null,
- dynamicRegistrations.folding ? null : true, // foldingRangeProvider
- null, // declarationProvider
- ExecuteCommandOptions(Commands.serverSupportedCommands),
- ServerCapabilitiesWorkspace(
- ServerCapabilitiesWorkspaceFolders(true, true)),
- null);
-
+ server.capabilities = server.capabilitiesComputer
+ .computeServerCapabilities(params.capabilities);
return success(InitializeResult(server.capabilities));
}
}
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_initialized.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_initialized.dart
index 854eaf5..f1cbeba 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_initialized.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_initialized.dart
@@ -4,7 +4,6 @@
import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
-import 'package:analysis_server/src/lsp/constants.dart';
import 'package:analysis_server/src/lsp/handlers/handler_states.dart';
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
import 'package:analysis_server/src/lsp/lsp_analysis_server.dart';
@@ -28,7 +27,7 @@
server,
);
- _performDynamicRegistration();
+ server.capabilitiesComputer.performDynamicRegistration();
if (!server.initializationOptions.onlyAnalyzeProjectsWithOpenFiles) {
server.setAnalysisRoots(openWorkspacePaths);
@@ -36,146 +35,4 @@
return success();
}
-
- /// 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 dartFiles = DocumentFilter('dart', 'file', null);
- final pubspecFile = DocumentFilter('yaml', 'file', '**/pubspec.yaml');
- final analysisOptionsFile =
- DocumentFilter('yaml', 'file', '**/analysis_options.yaml');
- final allTypes = [dartFiles, pubspecFile, analysisOptionsFile];
-
- // TODO(dantup): When we support plugins, we will need to collect their
- // requirements too. For example, the Angular plugin might wish to add HTML
- // `DocumentFilter('html', 'file', null)` to many of these requests.
-
- var _lastRegistrationId = 1;
- final registrations = <Registration>[];
-
- /// Helper for creating registrations with IDs.
- void register(bool condition, Method method, [ToJsonable options]) {
- if (condition == true) {
- registrations.add(Registration(
- (_lastRegistrationId++).toString(), method.toJson(), options));
- }
- }
-
- final textCapabilities = server.clientCapabilities?.textDocument;
-
- register(
- textCapabilities?.synchronization?.dynamicRegistration,
- Method.textDocument_didOpen,
- TextDocumentRegistrationOptions(allTypes),
- );
- register(
- textCapabilities?.synchronization?.dynamicRegistration,
- Method.textDocument_didClose,
- TextDocumentRegistrationOptions(allTypes),
- );
- register(
- textCapabilities?.synchronization?.dynamicRegistration,
- Method.textDocument_didChange,
- TextDocumentChangeRegistrationOptions(
- TextDocumentSyncKind.Incremental, allTypes),
- );
- register(
- server.clientCapabilities?.textDocument?.completion?.dynamicRegistration,
- Method.textDocument_completion,
- CompletionRegistrationOptions(
- dartCompletionTriggerCharacters,
- null,
- true,
- [dartFiles],
- ),
- );
- register(
- textCapabilities?.hover?.dynamicRegistration,
- Method.textDocument_hover,
- TextDocumentRegistrationOptions([dartFiles]),
- );
- register(
- textCapabilities?.signatureHelp?.dynamicRegistration,
- Method.textDocument_signatureHelp,
- SignatureHelpRegistrationOptions(
- dartSignatureHelpTriggerCharacters, [dartFiles]),
- );
- register(
- server.clientCapabilities?.textDocument?.references?.dynamicRegistration,
- Method.textDocument_references,
- TextDocumentRegistrationOptions([dartFiles]),
- );
- register(
- textCapabilities?.documentHighlight?.dynamicRegistration,
- Method.textDocument_documentHighlight,
- TextDocumentRegistrationOptions([dartFiles]),
- );
- register(
- textCapabilities?.documentSymbol?.dynamicRegistration,
- Method.textDocument_documentSymbol,
- TextDocumentRegistrationOptions([dartFiles]),
- );
- register(
- server.clientCapabilities?.textDocument?.formatting?.dynamicRegistration,
- Method.textDocument_formatting,
- TextDocumentRegistrationOptions([dartFiles]),
- );
- register(
- textCapabilities?.onTypeFormatting?.dynamicRegistration,
- Method.textDocument_onTypeFormatting,
- DocumentOnTypeFormattingRegistrationOptions(
- dartTypeFormattingCharacters.first,
- dartTypeFormattingCharacters.skip(1).toList(),
- [dartFiles],
- ),
- );
- register(
- server.clientCapabilities?.textDocument?.definition?.dynamicRegistration,
- Method.textDocument_definition,
- TextDocumentRegistrationOptions([dartFiles]),
- );
- register(
- textCapabilities?.implementation?.dynamicRegistration,
- Method.textDocument_implementation,
- TextDocumentRegistrationOptions([dartFiles]),
- );
- register(
- server.clientCapabilities?.textDocument?.codeAction?.dynamicRegistration,
- Method.textDocument_codeAction,
- CodeActionRegistrationOptions(
- [dartFiles], DartCodeActionKind.serverSupportedKinds),
- );
- register(
- textCapabilities?.rename?.dynamicRegistration,
- Method.textDocument_rename,
- RenameRegistrationOptions(true, [dartFiles]),
- );
- register(
- textCapabilities?.foldingRange?.dynamicRegistration,
- Method.textDocument_foldingRange,
- TextDocumentRegistrationOptions([dartFiles]),
- );
-
- // Only send the registration request if we have at least one (since
- // otherwise we don't know that the client supports registerCapability).
- if (registrations.isNotEmpty) {
- final registrationResponse = await server.sendRequest(
- Method.client_registerCapability,
- RegistrationParams(registrations),
- );
-
- if (registrationResponse.error != null) {
- server.logErrorToClient(
- 'Failed to register capabilities with client: '
- '(${registrationResponse.error.code}) '
- '${registrationResponse.error.message}',
- );
- }
- }
- }
}
diff --git a/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart b/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
index c91a44c..cecd4f4 100644
--- a/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
+++ b/pkg/analysis_server/lib/src/lsp/lsp_analysis_server.dart
@@ -11,6 +11,7 @@
import 'package:analysis_server/protocol/protocol_generated.dart' as protocol;
import 'package:analysis_server/src/analysis_server.dart';
import 'package:analysis_server/src/analysis_server_abstract.dart';
+import 'package:analysis_server/src/channel/channel.dart';
import 'package:analysis_server/src/collections.dart';
import 'package:analysis_server/src/computer/computer_closingLabels.dart';
import 'package:analysis_server/src/computer/computer_outline.dart';
@@ -23,7 +24,9 @@
import 'package:analysis_server/src/lsp/handlers/handler_states.dart';
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
import 'package:analysis_server/src/lsp/mapping.dart';
+import 'package:analysis_server/src/lsp/server_capabilities_computer.dart';
import 'package:analysis_server/src/plugin/notification_manager.dart';
+import 'package:analysis_server/src/plugin/plugin_manager.dart';
import 'package:analysis_server/src/protocol_server.dart' as protocol;
import 'package:analysis_server/src/server/crash_reporting_attachments.dart';
import 'package:analysis_server/src/server/diagnostic_server.dart';
@@ -42,6 +45,7 @@
import 'package:analyzer/src/dart/analysis/status.dart' as nd;
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/sdk.dart';
+import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
import 'package:watcher/watcher.dart';
/// Instances of the class [LspAnalysisServer] implement an LSP-based server
@@ -81,6 +85,7 @@
/// Capabilities of the server. Will be null prior to initialization as
/// the server capabilities depend on the client capabilities.
ServerCapabilities capabilities;
+ ServerCapabilitiesComputer capabilitiesComputer;
LspPerformance performanceStats = LspPerformance();
@@ -88,6 +93,8 @@
/// automatically.
bool willExit = false;
+ StreamSubscription _pluginChangeSubscription;
+
/// Initialize a newly created server to send and receive messages to the
/// given [channel].
LspAnalysisServer(
@@ -99,14 +106,19 @@
InstrumentationService instrumentationService, {
DiagnosticServer diagnosticServer,
}) : super(
- options,
- sdkManager,
- diagnosticServer,
- crashReportingAttachmentsBuilder,
- baseResourceProvider,
- instrumentationService,
- NullNotificationManager()) {
+ options,
+ sdkManager,
+ diagnosticServer,
+ crashReportingAttachmentsBuilder,
+ baseResourceProvider,
+ instrumentationService,
+ NotificationManager(
+ const NoOpServerCommunicationChannel(),
+ baseResourceProvider.pathContext,
+ ),
+ ) {
messageHandler = UninitializedStateMessageHandler(this);
+ capabilitiesComputer = ServerCapabilitiesComputer(this);
final contextManagerCallbacks =
LspServerContextManagerCallbacks(this, resourceProvider);
@@ -116,6 +128,8 @@
analysisDriverScheduler.start();
channel.listen(handleMessage, onDone: done, onError: socketError);
+ _pluginChangeSubscription =
+ pluginManager.pluginsChanged.listen((_) => _onPluginsChanged());
}
/// The capabilities of the LSP client. Will be null prior to initialization.
@@ -127,6 +141,16 @@
/// specific server functionality. Will be null prior to initialization.
LspInitializationOptions get initializationOptions => _initializationOptions;
+ @override
+ set pluginManager(PluginManager value) {
+ // we exchange the plugin manager in tests
+ super.pluginManager = value;
+ _pluginChangeSubscription?.cancel();
+
+ _pluginChangeSubscription =
+ pluginManager.pluginsChanged.listen((_) => _onPluginsChanged());
+ }
+
RefactoringWorkspace get refactoringWorkspace => _refactoringWorkspace ??=
RefactoringWorkspace(driverMap.values, searchEngine);
@@ -134,7 +158,7 @@
final didAdd = priorityFiles.add(path);
assert(didAdd);
if (didAdd) {
- _updateDriversPriorityFiles();
+ _updateDriversAndPluginsPriorityFiles();
}
}
@@ -315,7 +339,7 @@
final didRemove = priorityFiles.remove(path);
assert(didRemove);
if (didRemove) {
- _updateDriversPriorityFiles();
+ _updateDriversAndPluginsPriorityFiles();
}
}
@@ -398,6 +422,7 @@
declarationsTracker?.discardContexts();
final uniquePaths = HashSet<String>.of(includedPaths ?? const []);
contextManager.setRoots(uniquePaths.toList(), [], {});
+ notificationManager.setAnalysisRoots(includedPaths, []);
addContextsToDeclarationsTracker();
}
@@ -457,6 +482,7 @@
Future(() {
channel.close();
});
+ _pluginChangeSubscription?.cancel();
return Future.value();
}
@@ -491,9 +517,32 @@
notifyFlutterWidgetDescriptions(path);
}
- void _updateDriversPriorityFiles() {
+ void _onPluginsChanged() {
+ capabilitiesComputer.performDynamicRegistration();
+ }
+
+ void _updateDriversAndPluginsPriorityFiles() {
+ final priorityFilesList = priorityFiles.toList();
driverMap.values.forEach((driver) {
- driver.priorityFiles = priorityFiles.toList();
+ driver.priorityFiles = priorityFilesList;
+ });
+
+ final pluginPriorities =
+ plugin.AnalysisSetPriorityFilesParams(priorityFilesList);
+ pluginManager.setAnalysisSetPriorityFilesParams(pluginPriorities);
+
+ // Plugins send most of their analysis results via notifications, but with
+ // LSP we're supposed to have them available per request. Assume that we'll
+ // only receive requests for files that are currently open.
+ final pluginSubscriptions = plugin.AnalysisSetSubscriptionsParams({
+ for (final service in plugin.AnalysisService.VALUES)
+ service: priorityFilesList,
+ });
+ pluginManager.setAnalysisSetSubscriptionsParams(pluginSubscriptions);
+
+ notificationManager.setSubscriptions({
+ for (final service in protocol.AnalysisService.VALUES)
+ service: priorityFiles
});
}
}
@@ -665,7 +714,19 @@
}
}
-class NullNotificationManager implements NotificationManager {
+class NoOpServerCommunicationChannel implements ServerCommunicationChannel {
+ const NoOpServerCommunicationChannel();
+
@override
- dynamic noSuchMethod(Invocation invocation) {}
+ void close() {}
+
+ @override
+ void listen(void Function(protocol.Request request) onRequest,
+ {Function onError, void Function() onDone}) {}
+
+ @override
+ void sendNotification(protocol.Notification notification) {}
+
+ @override
+ void sendResponse(protocol.Response response) {}
}
diff --git a/pkg/analysis_server/lib/src/lsp/server_capabilities_computer.dart b/pkg/analysis_server/lib/src/lsp/server_capabilities_computer.dart
new file mode 100644
index 0000000..a359d36
--- /dev/null
+++ b/pkg/analysis_server/lib/src/lsp/server_capabilities_computer.dart
@@ -0,0 +1,371 @@
+// 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/constants.dart';
+import 'package:analysis_server/src/lsp/lsp_analysis_server.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 intead 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_formatting,
+ Method.textDocument_onTypeFormatting,
+ Method.textDocument_definition,
+ Method.textDocument_codeAction,
+ Method.textDocument_rename,
+ Method.textDocument_foldingRange,
+ ];
+ final ClientCapabilities _capabilities;
+
+ ClientDynamicRegistrations(this._capabilities);
+
+ bool get codeActions =>
+ _capabilities.textDocument?.foldingRange?.dynamicRegistration ?? false;
+
+ bool get completion =>
+ _capabilities.textDocument?.completion?.dynamicRegistration ?? false;
+
+ bool get definition =>
+ _capabilities.textDocument?.definition?.dynamicRegistration ?? false;
+
+ bool get documentHighlights =>
+ _capabilities.textDocument?.documentHighlight?.dynamicRegistration ??
+ false;
+
+ bool get documentSymbol =>
+ _capabilities.textDocument?.documentSymbol?.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 references =>
+ _capabilities.textDocument?.references?.dynamicRegistration ?? false;
+
+ bool get rename =>
+ _capabilities.textDocument?.rename?.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 {
+ final LspAnalysisServer _server;
+
+ /// Map from method name to current registration data.
+ Map<String, Registration> _currentRegistrations = {};
+ var _lastRegistrationId = 0;
+
+ ServerCapabilitiesComputer(this._server);
+
+ ServerCapabilities computeServerCapabilities(
+ ClientCapabilities clientCapabilities) {
+ final codeActionLiteralSupport =
+ clientCapabilities.textDocument?.codeAction?.codeActionLiteralSupport;
+
+ final renameOptionsSupport =
+ clientCapabilities.textDocument?.rename?.prepareSupport ?? false;
+
+ final dynamicRegistrations = ClientDynamicRegistrations(clientCapabilities);
+
+ // 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(
+ dynamicRegistrations.textSync
+ ? null
+ : Either2<TextDocumentSyncOptions, num>.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.
+ true,
+ TextDocumentSyncKind.Incremental,
+ false,
+ false,
+ null,
+ )),
+ dynamicRegistrations.hover ? null : true, // hoverProvider
+ dynamicRegistrations.completion
+ ? null
+ : CompletionOptions(
+ true, // resolveProvider
+ dartCompletionTriggerCharacters,
+ ),
+ dynamicRegistrations.signatureHelp
+ ? null
+ : SignatureHelpOptions(
+ dartSignatureHelpTriggerCharacters,
+ ),
+ dynamicRegistrations.definition ? null : true, // definitionProvider
+ null,
+ dynamicRegistrations.implementation
+ ? null
+ : true, // implementationProvider
+ dynamicRegistrations.references ? null : true, // referencesProvider
+ dynamicRegistrations.documentHighlights
+ ? null
+ : true, // documentHighlightProvider
+ dynamicRegistrations.documentSymbol
+ ? null
+ : true, // documentSymbolProvider
+ true, // workspaceSymbolProvider
+ // "The `CodeActionOptions` return type is only valid if the client
+ // signals code action literal support via the property
+ // `textDocument.codeAction.codeActionLiteralSupport`."
+ dynamicRegistrations.codeActions
+ ? null
+ : codeActionLiteralSupport != null
+ ? Either2<bool, CodeActionOptions>.t2(
+ CodeActionOptions(DartCodeActionKind.serverSupportedKinds))
+ : Either2<bool, CodeActionOptions>.t1(true),
+ null,
+ dynamicRegistrations.formatting
+ ? null
+ : true, // documentFormattingProvider
+ false, // documentRangeFormattingProvider
+ dynamicRegistrations.typeFormatting
+ ? null
+ : DocumentOnTypeFormattingOptions(
+ dartTypeFormattingCharacters.first,
+ dartTypeFormattingCharacters.skip(1).toList()),
+ dynamicRegistrations.rename
+ ? null
+ : renameOptionsSupport
+ ? Either2<bool, RenameOptions>.t2(RenameOptions(true))
+ : Either2<bool, RenameOptions>.t1(true),
+ null,
+ null,
+ dynamicRegistrations.folding ? null : true, // foldingRangeProvider
+ null, // declarationProvider
+ ExecuteCommandOptions(Commands.serverSupportedCommands),
+ ServerCapabilitiesWorkspace(
+ ServerCapabilitiesWorkspaceFolders(true, true)),
+ null);
+ }
+
+ /// 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 dartFiles = DocumentFilter('dart', 'file', null);
+ final pubspecFile = DocumentFilter('yaml', 'file', '**/pubspec.yaml');
+ final analysisOptionsFile =
+ DocumentFilter('yaml', 'file', '**/analysis_options.yaml');
+
+ 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(null, 'file', '**/$glob'));
+
+ final allTypes = [
+ dartFiles,
+ pubspecFile,
+ analysisOptionsFile,
+ ...pluginTypes
+ ];
+
+ final registrations = <Registration>[];
+
+ /// Helper for creating registrations with IDs.
+ void register(bool condition, Method method, [ToJsonable options]) {
+ if (condition == true) {
+ registrations.add(Registration(
+ (_lastRegistrationId++).toString(), method.toJson(), options));
+ }
+ }
+
+ final textCapabilities = _server.clientCapabilities?.textDocument;
+
+ register(
+ textCapabilities?.synchronization?.dynamicRegistration,
+ Method.textDocument_didOpen,
+ TextDocumentRegistrationOptions(allTypes),
+ );
+ register(
+ textCapabilities?.synchronization?.dynamicRegistration,
+ Method.textDocument_didClose,
+ TextDocumentRegistrationOptions(allTypes),
+ );
+ register(
+ textCapabilities?.synchronization?.dynamicRegistration,
+ Method.textDocument_didChange,
+ TextDocumentChangeRegistrationOptions(
+ TextDocumentSyncKind.Incremental, allTypes),
+ );
+ register(
+ _server.clientCapabilities?.textDocument?.completion?.dynamicRegistration,
+ Method.textDocument_completion,
+ CompletionRegistrationOptions(
+ dartCompletionTriggerCharacters,
+ null,
+ true,
+ [dartFiles],
+ ),
+ );
+ register(
+ textCapabilities?.hover?.dynamicRegistration,
+ Method.textDocument_hover,
+ TextDocumentRegistrationOptions([dartFiles]),
+ );
+ register(
+ textCapabilities?.signatureHelp?.dynamicRegistration,
+ Method.textDocument_signatureHelp,
+ SignatureHelpRegistrationOptions(
+ dartSignatureHelpTriggerCharacters, [dartFiles]),
+ );
+ register(
+ _server.clientCapabilities?.textDocument?.references?.dynamicRegistration,
+ Method.textDocument_references,
+ TextDocumentRegistrationOptions([dartFiles]),
+ );
+ register(
+ textCapabilities?.documentHighlight?.dynamicRegistration,
+ Method.textDocument_documentHighlight,
+ TextDocumentRegistrationOptions([dartFiles]),
+ );
+ register(
+ textCapabilities?.documentSymbol?.dynamicRegistration,
+ Method.textDocument_documentSymbol,
+ TextDocumentRegistrationOptions([dartFiles]),
+ );
+ register(
+ _server.clientCapabilities?.textDocument?.formatting?.dynamicRegistration,
+ Method.textDocument_formatting,
+ TextDocumentRegistrationOptions([dartFiles]),
+ );
+ register(
+ textCapabilities?.onTypeFormatting?.dynamicRegistration,
+ Method.textDocument_onTypeFormatting,
+ DocumentOnTypeFormattingRegistrationOptions(
+ dartTypeFormattingCharacters.first,
+ dartTypeFormattingCharacters.skip(1).toList(),
+ [dartFiles],
+ ),
+ );
+ register(
+ _server.clientCapabilities?.textDocument?.definition?.dynamicRegistration,
+ Method.textDocument_definition,
+ TextDocumentRegistrationOptions([dartFiles]),
+ );
+ register(
+ textCapabilities?.implementation?.dynamicRegistration,
+ Method.textDocument_implementation,
+ TextDocumentRegistrationOptions([dartFiles]),
+ );
+ register(
+ _server.clientCapabilities?.textDocument?.codeAction?.dynamicRegistration,
+ Method.textDocument_codeAction,
+ CodeActionRegistrationOptions(
+ [dartFiles], DartCodeActionKind.serverSupportedKinds),
+ );
+ register(
+ textCapabilities?.rename?.dynamicRegistration,
+ Method.textDocument_rename,
+ RenameRegistrationOptions(true, [dartFiles]),
+ );
+ register(
+ textCapabilities?.foldingRange?.dynamicRegistration,
+ Method.textDocument_foldingRange,
+ TextDocumentRegistrationOptions(allTypes),
+ );
+
+ await _applyRegistrations(registrations);
+ }
+
+ Future<void> _applyRegistrations(List<Registration> registrations) async {
+ final newRegistrationsByMethod = {
+ for (final registration in registrations)
+ registration.method: registration
+ };
+
+ final additionalRegistrations = List.of(registrations);
+ final removedRegistrations = <Unregistration>[];
+
+ // compute a diff of old and new registrations to send the unregister or
+ // another register request. We assume that we'll only ever have one
+ // registration per LSP method name.
+ for (final entry in _currentRegistrations.entries) {
+ final method = entry.key;
+ final registration = entry.value;
+
+ final newRegistrationForMethod = newRegistrationsByMethod[method];
+ final entryRemovedOrChanged = newRegistrationForMethod?.registerOptions !=
+ registration.registerOptions;
+
+ if (entryRemovedOrChanged) {
+ removedRegistrations
+ .add(Unregistration(registration.id, registration.method));
+ } else {
+ additionalRegistrations.remove(newRegistrationForMethod);
+ }
+ }
+
+ _currentRegistrations = newRegistrationsByMethod;
+
+ if (removedRegistrations.isNotEmpty) {
+ await _server.sendRequest(Method.client_unregisterCapability,
+ UnregistrationParams(removedRegistrations));
+ }
+
+ // Only send the registration request if we have at least one (since
+ // otherwise we don't know that the client supports registerCapability).
+ if (additionalRegistrations.isNotEmpty) {
+ final registrationResponse = await _server.sendRequest(
+ Method.client_registerCapability,
+ RegistrationParams(additionalRegistrations),
+ );
+
+ if (registrationResponse.error != null) {
+ _server.logErrorToClient(
+ 'Failed to register capabilities with client: '
+ '(${registrationResponse.error.code}) '
+ '${registrationResponse.error.message}',
+ );
+ }
+ }
+ }
+}
diff --git a/pkg/analysis_server/lib/src/plugin/plugin_manager.dart b/pkg/analysis_server/lib/src/plugin/plugin_manager.dart
index de68843..75b4b4c 100644
--- a/pkg/analysis_server/lib/src/plugin/plugin_manager.dart
+++ b/pkg/analysis_server/lib/src/plugin/plugin_manager.dart
@@ -281,6 +281,8 @@
/// plugin has been started.
final Map<String, dynamic> _overlayState = <String, dynamic>{};
+ final StreamController<void> _pluginsChanged = StreamController.broadcast();
+
/// Initialize a newly created plugin manager. The notifications from the
/// running plugins will be handled by the given [notificationManager].
PluginManager(this.resourceProvider, this.byteStorePath, this.sdkPath,
@@ -289,6 +291,9 @@
/// Return a list of all of the plugins that are currently known.
List<PluginInfo> get plugins => _pluginMap.values.toList();
+ /// Stream emitting an event when known [plugins] change.
+ Stream<void> get pluginsChanged => _pluginsChanged.stream;
+
/// Add the plugin with the given [path] to the list of plugins that should be
/// used when analyzing code for the given [contextRoot]. If the plugin had
/// not yet been started, then it will be started by this method.
@@ -317,6 +322,7 @@
var session = await plugin.start(byteStorePath, sdkPath);
session?.onDone?.then((_) {
_pluginMap.remove(path);
+ _notifyPluginsChanged();
});
} catch (exception, stackTrace) {
// Record the exception (for debugging purposes) and record the fact
@@ -325,6 +331,8 @@
isNew = false;
}
}
+
+ _notifyPluginsChanged();
}
plugin.addContextRoot(contextRoot);
if (isNew) {
@@ -469,6 +477,7 @@
plugin.removeContextRoot(contextRoot);
if (plugin is DiscoveredPluginInfo && plugin.contextRoots.isEmpty) {
_pluginMap.remove(plugin.path);
+ _notifyPluginsChanged();
try {
plugin.stop();
} catch (e, st) {
@@ -708,6 +717,8 @@
return packagesFile;
}
+ void _notifyPluginsChanged() => _pluginsChanged.add(null);
+
/// Return the names of packages that are listed as dependencies in the given
/// [pubspecFile].
Iterable<String> _readDependecies(File pubspecFile) {
diff --git a/pkg/analysis_server/lib/src/utilities/mocks.dart b/pkg/analysis_server/lib/src/utilities/mocks.dart
index 1552225..1602ed4 100644
--- a/pkg/analysis_server/lib/src/utilities/mocks.dart
+++ b/pkg/analysis_server/lib/src/utilities/mocks.dart
@@ -148,6 +148,12 @@
Map<PluginInfo, Future<plugin.Response>> broadcastResults;
@override
+ List<PluginInfo> plugins = [];
+
+ StreamController<void> pluginsChangedController =
+ StreamController.broadcast();
+
+ @override
String get byteStorePath {
fail('Unexpected invocation of byteStorePath');
}
@@ -163,9 +169,7 @@
}
@override
- List<PluginInfo> get plugins {
- fail('Unexpected invocation of plugins');
- }
+ Stream<void> get pluginsChanged => pluginsChangedController.stream;
@override
ResourceProvider get resourceProvider {
diff --git a/pkg/analysis_server/test/lsp/folding_test.dart b/pkg/analysis_server/test/lsp/folding_test.dart
index b27ea0b..49dc5ce 100644
--- a/pkg/analysis_server/test/lsp/folding_test.dart
+++ b/pkg/analysis_server/test/lsp/folding_test.dart
@@ -3,6 +3,8 @@
// BSD-style license that can be found in the LICENSE file.
import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
+import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin;
+import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
@@ -66,6 +68,65 @@
expect(regions, unorderedEquals(expectedRegions));
}
+ Future<void> test_fromPlugins_dartFile() async {
+ final pluginAnalyzedFilePath = join(projectFolderPath, 'lib', 'foo.dart');
+ final pluginAnalyzedUri = Uri.file(pluginAnalyzedFilePath);
+
+ const content = '''
+ // [[contributed by fake plugin]]
+
+ class AnnotatedDartClass {[[
+ // content of dart class, contributed by server
+ ]]}
+ ''';
+ final ranges = rangesFromMarkers(content);
+ final withoutMarkers = withoutRangeMarkers(content);
+ newFile(pluginAnalyzedFilePath);
+
+ await initialize();
+ await openFile(pluginAnalyzedUri, withoutMarkers);
+
+ final pluginResult = plugin.AnalysisFoldingParams(
+ pluginAnalyzedFilePath,
+ [plugin.FoldingRegion(plugin.FoldingKind.DIRECTIVES, 7, 26)],
+ );
+ configureTestPlugin(notification: pluginResult.toNotification());
+
+ final res = await getFoldingRegions(pluginAnalyzedUri);
+ expect(
+ res,
+ unorderedEquals([
+ _toFoldingRange(ranges[0], FoldingRangeKind.Imports),
+ _toFoldingRange(ranges[1], null),
+ ]),
+ );
+ }
+
+ Future<void> test_fromPlugins_nonDartFile() async {
+ final pluginAnalyzedFilePath = join(projectFolderPath, 'lib', 'foo.sql');
+ final pluginAnalyzedUri = Uri.file(pluginAnalyzedFilePath);
+ const content = '''
+ CREATE TABLE foo(
+ [[-- some columns]]
+ );
+ ''';
+ final withoutMarkers = withoutRangeMarkers(content);
+ newFile(pluginAnalyzedFilePath, content: withoutMarkers);
+
+ await initialize();
+ await openFile(pluginAnalyzedUri, withoutMarkers);
+
+ final pluginResult = plugin.AnalysisFoldingParams(
+ pluginAnalyzedFilePath,
+ [plugin.FoldingRegion(plugin.FoldingKind.CLASS_BODY, 33, 15)],
+ );
+ configureTestPlugin(notification: pluginResult.toNotification());
+
+ final res = await getFoldingRegions(pluginAnalyzedUri);
+ final expectedRange = rangeFromMarkers(content);
+ expect(res, [_toFoldingRange(expectedRange, null)]);
+ }
+
Future<void> test_headersImportsComments() async {
// TODO(dantup): Review why the file header and the method comment ranges
// are different... one spans only the range to collapse, but the other
diff --git a/pkg/analysis_server/test/lsp/initialization_test.dart b/pkg/analysis_server/test/lsp/initialization_test.dart
index 0d0421f..2d132bb 100644
--- a/pkg/analysis_server/test/lsp/initialization_test.dart
+++ b/pkg/analysis_server/test/lsp/initialization_test.dart
@@ -5,7 +5,8 @@
import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
import 'package:analysis_server/src/lsp/constants.dart';
-import 'package:analysis_server/src/lsp/handlers/handler_initialize.dart';
+import 'package:analysis_server/src/lsp/server_capabilities_computer.dart';
+import 'package:analysis_server/src/plugin/plugin_manager.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
@@ -185,6 +186,95 @@
}
}
+ Future<void> test_dynamicRegistration_unregistersOutdatedAfterChange() async {
+ // Initialize by supporting dynamic registrations everywhere
+ List<Registration> registrations;
+ await handleExpectedRequest<ResponseMessage, RegistrationParams, void>(
+ Method.client_registerCapability,
+ () => initialize(
+ textDocumentCapabilities: withAllSupportedDynamicRegistrations(
+ emptyTextDocumentClientCapabilities)),
+ handler: (registrationParams) =>
+ registrations = registrationParams.registrations,
+ );
+
+ final unregisterRequest =
+ await expectRequest(Method.client_unregisterCapability, () {
+ final plugin = configureTestPlugin();
+ plugin.currentSession = PluginSession(plugin)
+ ..interestingFiles = ['*.foo'];
+ pluginManager.pluginsChangedController.add(null);
+ });
+ final unregistrations =
+ (unregisterRequest.params as UnregistrationParams).unregisterations;
+
+ // folding method should have been unregistered as the server now supports
+ // *.foo files for it as well.
+ final registrationIdForFolding = registrations
+ .singleWhere((r) => r.method == 'textDocument/foldingRange')
+ .id;
+ expect(
+ unregistrations,
+ contains(isA<Unregistration>()
+ .having((r) => r.method, 'method', 'textDocument/foldingRange')
+ .having((r) => r.id, 'id', registrationIdForFolding)),
+ );
+
+ // Renaming documents is a Dart-specific service that should not have been
+ // affected by the plugin change.
+ final registrationIdForRename =
+ registrations.singleWhere((r) => r.method == 'textDocument/rename').id;
+ expect(
+ unregistrations,
+ isNot(contains(isA<Unregistration>()
+ .having((r) => r.id, 'id', registrationIdForRename))));
+ }
+
+ Future<void> test_dynamicRegistration_updatesWithPlugins() async {
+ await initialize(
+ textDocumentCapabilities:
+ extendTextDocumentCapabilities(emptyTextDocumentClientCapabilities, {
+ 'foldingRange': {'dynamicRegistration': true},
+ }),
+ );
+
+ // The server will send an unregister request followed by another register
+ // request to change document filter on folding. We need to respond to the
+ // unregister request as the server awaits that.
+ requestsFromServer
+ .firstWhere((r) => r.method == Method.client_unregisterCapability)
+ .then((request) {
+ respondTo(request, null);
+ return (request.params as UnregistrationParams).unregisterations;
+ });
+
+ final request = await expectRequest(Method.client_registerCapability, () {
+ final plugin = configureTestPlugin();
+ plugin.currentSession = PluginSession(plugin)
+ ..interestingFiles = ['*.sql'];
+ pluginManager.pluginsChangedController.add(null);
+ });
+
+ final registrations = (request.params as RegistrationParams).registrations;
+
+ final documentFilterSql = DocumentFilter(null, 'file', '**/*.sql');
+ final documentFilterDart = DocumentFilter('dart', 'file', null);
+ final expectedFoldingRegistration =
+ isA<TextDocumentRegistrationOptions>().having(
+ (o) => o.documentSelector,
+ 'documentSelector',
+ containsAll([documentFilterSql, documentFilterDart]),
+ );
+
+ expect(
+ registrations,
+ contains(isA<Registration>()
+ .having((r) => r.method, 'method', 'textDocument/foldingRange')
+ .having((r) => r.registerOptions, 'registerOptions',
+ expectedFoldingRegistration)),
+ );
+ }
+
Future<void> test_initialize() async {
final response = await initialize();
expect(response, isNotNull);
diff --git a/pkg/analysis_server/test/lsp/server_abstract.dart b/pkg/analysis_server/test/lsp/server_abstract.dart
index ebd7388..b009198 100644
--- a/pkg/analysis_server/test/lsp/server_abstract.dart
+++ b/pkg/analysis_server/test/lsp/server_abstract.dart
@@ -49,11 +49,25 @@
@override
Stream<Message> get serverToClient => channel.serverToClient;
- void configureTestPlugin({plugin.ResponseResult respondWith}) {
- PluginInfo info = DiscoveredPluginInfo('a', 'b', 'c', null, null);
- pluginManager.broadcastResults = <PluginInfo, Future<plugin.Response>>{
- info: Future.value(respondWith.toResponse('-', 1))
- };
+ DiscoveredPluginInfo configureTestPlugin({
+ plugin.ResponseResult respondWith,
+ plugin.Notification notification,
+ }) {
+ final info = DiscoveredPluginInfo('a', 'b', 'c', null, null);
+ pluginManager.plugins.add(info);
+
+ if (respondWith != null) {
+ pluginManager.broadcastResults = <PluginInfo, Future<plugin.Response>>{
+ info: Future.value(respondWith.toResponse('-', 1))
+ };
+ }
+
+ if (notification != null) {
+ server.notificationManager
+ .handlePluginNotification(info.pluginId, notification);
+ }
+
+ return info;
}
/// Sends a request to the server and unwraps the result. Throws if the
diff --git a/pkg/analysis_server/tool/lsp_spec/README.md b/pkg/analysis_server/tool/lsp_spec/README.md
index da5c60f..3208c04 100644
--- a/pkg/analysis_server/tool/lsp_spec/README.md
+++ b/pkg/analysis_server/tool/lsp_spec/README.md
@@ -46,7 +46,7 @@
| window/logMessage | ✅ | | | |
| telemetry/event | | | | |
| client/registerCapability | ✅ | ✅ | ✅ | ✅ |
-| client/unregisterCapability | | | | | (unused, capabilities don't change currently)
+| client/unregisterCapability | ✅ | ✅ | ✅ | | Capabilities only change when using analyzer plugins
| workspace/workspaceFolders | | | | |
| workspace/didChangeWorkspaceFolders | ✅ | ✅ | ✅ | ✅ |
| workspace/configuration | | | | |