c5fb640747
This updates the `showUserPrompt` method to accept a cancellation token so that if requests are cancelled (by the client, or something like a second refactor cancelling the first), the prompt is also cancelled (and when the response arrives, it can be ignored). Note: Unfortunately the protocol (and VS Code) don't actually allow for the prompt to be cancelled/hidden from the user, this is mainly to avoid us keeping the request "alive" on the server if the user ignores a prompt and we know the parent request was cancelled anyway. Fixes https://github.com/dart-lang/sdk/issues/63285 Change-Id: Ia77880a749b284a2ff31c8cf560307ad908d2175 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/499360 Reviewed-by: Konstantin Shcheglov <scheglov@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
1446 lines
53 KiB
Dart
1446 lines
53 KiB
Dart
// Copyright (c) 2014, 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.
|
|
|
|
/// @docImport 'package:analysis_server_plugin/src/plugin_server.dart';
|
|
library;
|
|
|
|
import 'dart:async';
|
|
import 'dart:io' as io;
|
|
import 'dart:math' show max;
|
|
|
|
import 'package:analysis_server/lsp_protocol/protocol.dart' as lsp;
|
|
import 'package:analysis_server/protocol/protocol.dart';
|
|
import 'package:analysis_server/protocol/protocol_constants.dart';
|
|
import 'package:analysis_server/protocol/protocol_generated.dart'
|
|
hide AnalysisOptions, MessageType;
|
|
import 'package:analysis_server/src/analysis_server.dart';
|
|
import 'package:analysis_server/src/analytics/analytics_manager.dart';
|
|
import 'package:analysis_server/src/channel/channel.dart';
|
|
import 'package:analysis_server/src/computer/computer_highlights.dart';
|
|
import 'package:analysis_server/src/domains/analysis/occurrences.dart';
|
|
import 'package:analysis_server/src/domains/analysis/occurrences_dart.dart';
|
|
import 'package:analysis_server/src/flutter/flutter_notifications.dart';
|
|
import 'package:analysis_server/src/handler/legacy/analysis_get_errors.dart';
|
|
import 'package:analysis_server/src/handler/legacy/analysis_get_hover.dart';
|
|
import 'package:analysis_server/src/handler/legacy/analysis_get_imported_elements.dart';
|
|
import 'package:analysis_server/src/handler/legacy/analysis_get_navigation.dart';
|
|
import 'package:analysis_server/src/handler/legacy/analysis_get_signature.dart';
|
|
import 'package:analysis_server/src/handler/legacy/analysis_reanalyze.dart';
|
|
import 'package:analysis_server/src/handler/legacy/analysis_set_analysis_roots.dart';
|
|
import 'package:analysis_server/src/handler/legacy/analysis_set_general_subscriptions.dart';
|
|
import 'package:analysis_server/src/handler/legacy/analysis_set_priority_files.dart';
|
|
import 'package:analysis_server/src/handler/legacy/analysis_set_subscriptions.dart';
|
|
import 'package:analysis_server/src/handler/legacy/analysis_update_content.dart';
|
|
import 'package:analysis_server/src/handler/legacy/analysis_update_options.dart';
|
|
import 'package:analysis_server/src/handler/legacy/analytics_enable.dart';
|
|
import 'package:analysis_server/src/handler/legacy/analytics_is_enabled.dart';
|
|
import 'package:analysis_server/src/handler/legacy/analytics_send_event.dart';
|
|
import 'package:analysis_server/src/handler/legacy/analytics_send_timing.dart';
|
|
import 'package:analysis_server/src/handler/legacy/completion_get_suggestion_details2.dart';
|
|
import 'package:analysis_server/src/handler/legacy/completion_get_suggestions2.dart';
|
|
import 'package:analysis_server/src/handler/legacy/diagnostic_get_diagnostics.dart';
|
|
import 'package:analysis_server/src/handler/legacy/diagnostic_get_server_port.dart';
|
|
import 'package:analysis_server/src/handler/legacy/edit_bulk_fixes.dart';
|
|
import 'package:analysis_server/src/handler/legacy/edit_format.dart';
|
|
import 'package:analysis_server/src/handler/legacy/edit_format_if_enabled.dart';
|
|
import 'package:analysis_server/src/handler/legacy/edit_get_assists.dart';
|
|
import 'package:analysis_server/src/handler/legacy/edit_get_available_refactorings.dart';
|
|
import 'package:analysis_server/src/handler/legacy/edit_get_fixes.dart';
|
|
import 'package:analysis_server/src/handler/legacy/edit_get_postfix_completion.dart';
|
|
import 'package:analysis_server/src/handler/legacy/edit_get_refactoring.dart';
|
|
import 'package:analysis_server/src/handler/legacy/edit_get_statement_completion.dart';
|
|
import 'package:analysis_server/src/handler/legacy/edit_import_elements.dart';
|
|
import 'package:analysis_server/src/handler/legacy/edit_is_postfix_completion_applicable.dart';
|
|
import 'package:analysis_server/src/handler/legacy/edit_list_postfix_completion_templates.dart';
|
|
import 'package:analysis_server/src/handler/legacy/edit_organize_directives.dart';
|
|
import 'package:analysis_server/src/handler/legacy/edit_sort_members.dart';
|
|
import 'package:analysis_server/src/handler/legacy/execution_create_context.dart';
|
|
import 'package:analysis_server/src/handler/legacy/execution_delete_context.dart';
|
|
import 'package:analysis_server/src/handler/legacy/execution_get_suggestions.dart';
|
|
import 'package:analysis_server/src/handler/legacy/execution_map_uri.dart';
|
|
import 'package:analysis_server/src/handler/legacy/execution_set_subscriptions.dart';
|
|
import 'package:analysis_server/src/handler/legacy/flutter_get_widget_description.dart';
|
|
import 'package:analysis_server/src/handler/legacy/flutter_set_subscriptions.dart';
|
|
import 'package:analysis_server/src/handler/legacy/flutter_set_widget_property_value.dart';
|
|
import 'package:analysis_server/src/handler/legacy/legacy_handler.dart';
|
|
import 'package:analysis_server/src/handler/legacy/lsp_over_legacy_handler.dart';
|
|
import 'package:analysis_server/src/handler/legacy/search_find_element_references.dart';
|
|
import 'package:analysis_server/src/handler/legacy/search_find_member_declarations.dart';
|
|
import 'package:analysis_server/src/handler/legacy/search_find_member_references.dart';
|
|
import 'package:analysis_server/src/handler/legacy/search_find_top_level_declarations.dart';
|
|
import 'package:analysis_server/src/handler/legacy/search_get_element_declarations.dart';
|
|
import 'package:analysis_server/src/handler/legacy/search_get_type_hierarchy.dart';
|
|
import 'package:analysis_server/src/handler/legacy/server_cancel_request.dart';
|
|
import 'package:analysis_server/src/handler/legacy/server_get_version.dart';
|
|
import 'package:analysis_server/src/handler/legacy/server_set_client_capabilities.dart';
|
|
import 'package:analysis_server/src/handler/legacy/server_set_subscriptions.dart';
|
|
import 'package:analysis_server/src/handler/legacy/server_shutdown.dart';
|
|
import 'package:analysis_server/src/handler/legacy/unsupported_request.dart';
|
|
import 'package:analysis_server/src/lsp/client_capabilities.dart' as lsp;
|
|
import 'package:analysis_server/src/lsp/client_configuration.dart' as lsp;
|
|
import 'package:analysis_server/src/lsp/constants.dart' as lsp;
|
|
import 'package:analysis_server/src/lsp/handlers/handler_states.dart';
|
|
import 'package:analysis_server/src/operation/operation_analysis.dart';
|
|
import 'package:analysis_server/src/plugin/notification_manager.dart';
|
|
import 'package:analysis_server/src/protocol_server.dart' as server;
|
|
import 'package:analysis_server/src/scheduler/scheduled_message.dart';
|
|
import 'package:analysis_server/src/server/crash_reporting_attachments.dart';
|
|
import 'package:analysis_server/src/server/debounce_requests.dart';
|
|
import 'package:analysis_server/src/server/detachable_filesystem_manager.dart';
|
|
import 'package:analysis_server/src/server/diagnostic_server.dart';
|
|
import 'package:analysis_server/src/server/error_notifier.dart';
|
|
import 'package:analysis_server/src/server/features.dart';
|
|
import 'package:analysis_server/src/server/sdk_configuration.dart';
|
|
import 'package:analysis_server/src/services/completion/completion_state.dart';
|
|
import 'package:analysis_server/src/services/execution/execution_context.dart';
|
|
import 'package:analysis_server/src/services/flutter/widget_descriptions.dart';
|
|
import 'package:analysis_server/src/services/refactoring/legacy/refactoring_manager.dart';
|
|
import 'package:analysis_server/src/session_logger/session_logger.dart';
|
|
import 'package:analysis_server/src/status/performance_logger.dart';
|
|
import 'package:analysis_server/src/utilities/extensions/resource_provider.dart';
|
|
import 'package:analysis_server/src/utilities/process.dart';
|
|
import 'package:analysis_server_plugin/src/correction/performance.dart';
|
|
import 'package:analyzer/dart/analysis/results.dart';
|
|
import 'package:analyzer/dart/analysis/session.dart';
|
|
import 'package:analyzer/dart/ast/ast.dart';
|
|
import 'package:analyzer/exception/exception.dart';
|
|
import 'package:analyzer/file_system/file_system.dart';
|
|
import 'package:analyzer/instrumentation/instrumentation.dart';
|
|
import 'package:analyzer/src/dart/analysis/analysis_options.dart';
|
|
import 'package:analyzer/src/dart/analysis/status.dart' as analysis;
|
|
import 'package:analyzer/src/generated/sdk.dart';
|
|
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
|
|
import 'package:analyzer/src/util/performance/operation_performance.dart';
|
|
import 'package:analyzer/src/utilities/cancellation.dart';
|
|
import 'package:analyzer_plugin/protocol/protocol_common.dart' hide Element;
|
|
import 'package:analyzer_plugin/src/utilities/client_uri_converter.dart';
|
|
import 'package:analyzer_plugin/src/utilities/navigation/navigation.dart';
|
|
import 'package:analyzer_plugin/src/utilities/navigation/navigation_dart.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:language_server_protocol/json_parsing.dart' as lsp;
|
|
import 'package:meta/meta.dart';
|
|
import 'package:telemetry/crash_reporting.dart';
|
|
import 'package:watcher/watcher.dart';
|
|
|
|
/// A function that can be executed to create a handler for a request.
|
|
typedef HandlerGenerator =
|
|
LegacyHandler Function(
|
|
LegacyAnalysisServer,
|
|
Request,
|
|
CancellationToken,
|
|
OperationPerformanceImpl,
|
|
);
|
|
|
|
typedef OptionUpdater = void Function(AnalysisOptionsImpl options);
|
|
|
|
/// Various IDE options.
|
|
class AnalysisServerOptions {
|
|
String? newAnalysisDriverLog;
|
|
|
|
String? clientId;
|
|
String? clientVersion;
|
|
|
|
/// Base path where to cache data.
|
|
String? cacheFolder;
|
|
|
|
/// If [true] don't create a (Evicting)FileByteStore.
|
|
///
|
|
/// Intended for benchmarking as it makes "instructions:u" in `perf stat` more
|
|
/// stable, although it then doesn't benchmark that code.
|
|
bool? disableFileByteStore;
|
|
|
|
/// The path to the package config file override.
|
|
/// If `null`, then the default discovery mechanism is used.
|
|
String? packagesFile;
|
|
|
|
/// The crash report sender instance; note, this object can be `null`, and
|
|
/// should be accessed via a null-aware operator.
|
|
CrashReportSender? crashReportSender;
|
|
|
|
/// An optional set of configuration overrides specified by the SDK.
|
|
///
|
|
/// These overrides can provide new values for configuration settings, and are
|
|
/// generally used in specific SDKs (like the internal google3 one).
|
|
SdkConfiguration? configurationOverrides;
|
|
|
|
/// Whether to use the Language Server Protocol.
|
|
bool useLanguageServerProtocol = false;
|
|
|
|
/// The set of enabled features.
|
|
FeatureSet featureSet = FeatureSet();
|
|
|
|
/// If set, this string will be reported as the protocol version.
|
|
String? reportProtocolVersion;
|
|
|
|
/// Experiments which have been enabled (or disabled) via the
|
|
/// `--enable-experiment` command-line option.
|
|
List<String> enabledExperiments = [];
|
|
|
|
/// Whether to enable fine-grained dependencies.
|
|
bool withFineDependencies = false;
|
|
|
|
bool usePlugins = true;
|
|
}
|
|
|
|
/// Instances of the class [LegacyAnalysisServer] implement a server that
|
|
/// listens on a [ServerCommunicationChannel] for analysis requests and
|
|
/// processes them.
|
|
class LegacyAnalysisServer extends AnalysisServer {
|
|
/// A map from the name of a request to a function used to create a request
|
|
/// handler.
|
|
///
|
|
/// Requests that don't match anything in this map will be passed to
|
|
/// [LspOverLegacyHandler] for possible handling before returning an error.
|
|
static final Map<String, HandlerGenerator> requestHandlerGenerators = {
|
|
analysisRequestGetErrors: AnalysisGetErrorsHandler.new,
|
|
analysisRequestGetHover: AnalysisGetHoverHandler.new,
|
|
analysisRequestGetImportedElements: AnalysisGetImportedElementsHandler.new,
|
|
analysisRequestGetLibraryDependencies: UnsupportedRequestHandler.new,
|
|
analysisRequestGetNavigation: AnalysisGetNavigationHandler.new,
|
|
analysisRequestGetReachableSources: UnsupportedRequestHandler.new,
|
|
analysisRequestGetSignature: AnalysisGetSignatureHandler.new,
|
|
analysisRequestReanalyze: AnalysisReanalyzeHandler.new,
|
|
analysisRequestSetAnalysisRoots: AnalysisSetAnalysisRootsHandler.new,
|
|
analysisRequestSetGeneralSubscriptions:
|
|
AnalysisSetGeneralSubscriptionsHandler.new,
|
|
analysisRequestSetPriorityFiles: AnalysisSetPriorityFilesHandler.new,
|
|
analysisRequestSetSubscriptions: AnalysisSetSubscriptionsHandler.new,
|
|
analysisRequestUpdateContent: AnalysisUpdateContentHandler.new,
|
|
analysisRequestUpdateOptions: AnalysisUpdateOptionsHandler.new,
|
|
|
|
analyticsRequestIsEnabled: AnalyticsIsEnabledHandler.new,
|
|
analyticsRequestEnable: AnalyticsEnableHandler.new,
|
|
analyticsRequestSendEvent: AnalyticsSendEventHandler.new,
|
|
analyticsRequestSendTiming: AnalyticsSendTimingHandler.new,
|
|
|
|
completionRequestGetSuggestionDetails2:
|
|
CompletionGetSuggestionDetails2Handler.new,
|
|
completionRequestGetSuggestions2: CompletionGetSuggestions2Handler.new,
|
|
|
|
diagnosticRequestGetDiagnostics: DiagnosticGetDiagnosticsHandler.new,
|
|
diagnosticRequestGetServerPort: DiagnosticGetServerPortHandler.new,
|
|
|
|
editRequestFormat: EditFormatHandler.new,
|
|
editRequestFormatIfEnabled: EditFormatIfEnabledHandler.new,
|
|
editRequestGetAssists: EditGetAssistsHandler.new,
|
|
editRequestGetAvailableRefactorings:
|
|
EditGetAvailableRefactoringsHandler.new,
|
|
editRequestBulkFixes: EditBulkFixes.new,
|
|
editRequestGetFixes: EditGetFixesHandler.new,
|
|
editRequestGetRefactoring: EditGetRefactoringHandler.new,
|
|
editRequestImportElements: EditImportElementsHandler.new,
|
|
editRequestOrganizeDirectives: EditOrganizeDirectivesHandler.new,
|
|
editRequestSortMembers: EditSortMembersHandler.new,
|
|
editRequestGetStatementCompletion: EditGetStatementCompletionHandler.new,
|
|
editRequestIsPostfixCompletionApplicable:
|
|
EditIsPostfixCompletionApplicableHandler.new,
|
|
editRequestGetPostfixCompletion: EditGetPostfixCompletionHandler.new,
|
|
editRequestListPostfixCompletionTemplates:
|
|
EditListPostfixCompletionTemplatesHandler.new,
|
|
|
|
executionRequestCreateContext: ExecutionCreateContextHandler.new,
|
|
executionRequestDeleteContext: ExecutionDeleteContextHandler.new,
|
|
executionRequestGetSuggestions: ExecutionGetSuggestionsHandler.new,
|
|
executionRequestMapUri: ExecutionMapUriHandler.new,
|
|
executionRequestSetSubscriptions: ExecutionSetSubscriptionsHandler.new,
|
|
|
|
flutterRequestGetWidgetDescription: FlutterGetWidgetDescriptionHandler.new,
|
|
flutterRequestSetWidgetPropertyValue:
|
|
FlutterSetWidgetPropertyValueHandler.new,
|
|
flutterRequestSetSubscriptions: FlutterSetSubscriptionsHandler.new,
|
|
|
|
searchRequestFindElementReferences: SearchFindElementReferencesHandler.new,
|
|
searchRequestFindMemberDeclarations:
|
|
SearchFindMemberDeclarationsHandler.new,
|
|
searchRequestFindMemberReferences: SearchFindMemberReferencesHandler.new,
|
|
searchRequestFindTopLevelDeclarations:
|
|
SearchFindTopLevelDeclarationsHandler.new,
|
|
searchRequestGetElementDeclarations:
|
|
SearchGetElementDeclarationsHandler.new,
|
|
searchRequestGetTypeHierarchy: SearchGetTypeHierarchyHandler.new,
|
|
|
|
serverRequestCancelRequest: ServerCancelRequestHandler.new,
|
|
serverRequestGetVersion: ServerGetVersionHandler.new,
|
|
serverRequestSetClientCapabilities: ServerSetClientCapabilitiesHandler.new,
|
|
serverRequestSetSubscriptions: ServerSetSubscriptionsHandler.new,
|
|
serverRequestShutdown: ServerShutdownHandler.new,
|
|
|
|
lspRequestHandle: LspOverLegacyHandler.new,
|
|
};
|
|
|
|
/// The channel from which requests are received and to which responses should
|
|
/// be sent.
|
|
final ServerCommunicationChannel channel;
|
|
|
|
@override
|
|
late final FutureOr<InitializedStateMessageHandler> lspInitialized =
|
|
InitializedStateMessageHandler(this);
|
|
|
|
/// Whether either the last status message sent to the client or the last
|
|
/// status message sent from any [PluginServer] indicated `isWorking: true`.
|
|
@visibleForTesting
|
|
bool statusAnalyzing = false;
|
|
|
|
/// Whether the analysis server is currently analyzing (not including any
|
|
/// plugins).
|
|
bool serverStatusAnalyzing = false;
|
|
|
|
/// A set of the [ServerService]s to send notifications for.
|
|
Set<ServerService> serverServices = {};
|
|
|
|
/// A table mapping request ids to cancellation tokens that allow cancelling
|
|
/// the request.
|
|
///
|
|
/// Tokens are removed once a request completes and should not be assumed to
|
|
/// exist in this table just because cancellation was requested.
|
|
Map<String, CancelableToken> cancellationTokens = {};
|
|
|
|
/// A set of the [GeneralAnalysisService]s to send notifications for.
|
|
Set<GeneralAnalysisService> generalAnalysisServices = {};
|
|
|
|
/// A table mapping [AnalysisService]s to the file paths for which these
|
|
/// notifications should be sent.
|
|
Map<AnalysisService, Set<String>> analysisServices = {};
|
|
|
|
/// The most recently registered set of client capabilities. The default is to
|
|
/// have no registered requests and no additional capabilities.
|
|
///
|
|
/// Must be modified through the [clientCapabilities] setter.
|
|
ServerSetClientCapabilitiesParams _clientCapabilities =
|
|
ServerSetClientCapabilitiesParams([]);
|
|
|
|
/// See [editorClientCapabilities].
|
|
var _editorClientCapabilities = lsp.fixedBasicLspClientCapabilities;
|
|
|
|
@override
|
|
final lsp.LspClientConfiguration lspClientConfiguration;
|
|
|
|
/// A table mapping [FlutterService]s to the file paths for which these
|
|
/// notifications should be sent.
|
|
Map<FlutterService, Set<String>> flutterServices = {};
|
|
|
|
/// The support for Flutter properties.
|
|
WidgetDescriptions flutterWidgetDescriptions = WidgetDescriptions();
|
|
|
|
/// The state used by the completion domain handlers.
|
|
final CompletionState completionState = CompletionState();
|
|
|
|
/// The object used to manage uncompleted refactorings.
|
|
late RefactoringManager? _refactoringManager;
|
|
|
|
/// The context used by the execution domain handlers.
|
|
final ExecutionContext executionContext = ExecutionContext();
|
|
|
|
/// The next search response id.
|
|
int nextSearchId = 0;
|
|
|
|
/// The [Completer] that completes when analysis is complete.
|
|
///
|
|
/// This Completer is not used for communicating to the client whether we are
|
|
/// analyzing; it is only used by some 'search_find' handlers, and in some tests.
|
|
Completer<void>? _onAnalysisCompleteCompleter;
|
|
|
|
/// The controller that is notified when analysis is started.
|
|
final StreamController<bool> _onAnalysisStartedController =
|
|
StreamController.broadcast();
|
|
|
|
/// If the "analysis.analyzedFiles" notification is currently being subscribed
|
|
/// to (see [generalAnalysisServices]), and at least one such notification has
|
|
/// been sent since the subscription was enabled, the set of analyzed files
|
|
/// that was delivered in the most recently sent notification. Otherwise
|
|
/// `null`.
|
|
Set<String>? prevAnalyzedFiles;
|
|
|
|
/// The controller for [onAnalysisSetChanged].
|
|
final StreamController<void> _onAnalysisSetChangedController =
|
|
StreamController.broadcast(sync: true);
|
|
|
|
/// An optional manager to handle file systems which may not always be
|
|
/// available.
|
|
final DetachableFileSystemManager? detachableFileSystemManager;
|
|
|
|
/// The broadcast stream of requests that were discarded because there
|
|
/// was another request that made this one irrelevant.
|
|
@visibleForTesting
|
|
final StreamController<Request> discardedRequests =
|
|
StreamController.broadcast(sync: true);
|
|
|
|
/// The index of the next request from the server to the client.
|
|
int nextServerRequestId = 0;
|
|
|
|
/// A table mapping the ids of requests sent from the server to the client
|
|
/// that have not yet received a response, to the completer used to return the
|
|
/// response when it has been received.
|
|
Map<String, Completer<Response>> pendingServerRequests = {};
|
|
|
|
/// Whether the server should send LSP notifications.
|
|
///
|
|
/// This is set once the client sends any LSP request or client capability
|
|
/// that depends on LSP functionality.
|
|
bool sendLspNotifications = false;
|
|
|
|
/// Initialize a newly created server to receive requests from and send
|
|
/// responses to the given [channel].
|
|
LegacyAnalysisServer(
|
|
this.channel,
|
|
ResourceProvider baseResourceProvider,
|
|
AnalysisServerOptions options,
|
|
DartSdkManager sdkManager,
|
|
AnalyticsManager analyticsManager,
|
|
CrashReportingAttachmentsBuilder crashReportingAttachmentsBuilder,
|
|
InstrumentationService instrumentationService,
|
|
SessionLogger sessionLogger, {
|
|
http.Client? httpClient,
|
|
ProcessRunner? processRunner,
|
|
super.requestStatistics,
|
|
DiagnosticServer? diagnosticServer,
|
|
this.detachableFileSystemManager,
|
|
super.enableBlazeWatcher,
|
|
super.dartFixPromptManager,
|
|
super.providedByteStore,
|
|
super.pluginManager,
|
|
super.messageSchedulerListener,
|
|
PerformanceLogger? performanceLogger,
|
|
super.environment,
|
|
}) : lspClientConfiguration = lsp.LspClientConfiguration(
|
|
baseResourceProvider.pathContext,
|
|
),
|
|
super(
|
|
options,
|
|
sdkManager,
|
|
diagnosticServer,
|
|
analyticsManager,
|
|
crashReportingAttachmentsBuilder,
|
|
baseResourceProvider,
|
|
instrumentationService,
|
|
sessionLogger,
|
|
httpClient,
|
|
processRunner,
|
|
NotificationManager(channel, baseResourceProvider.pathContext),
|
|
usePlugins: options.usePlugins,
|
|
) {
|
|
var contextManagerCallbacks = ServerContextManagerCallbacks(
|
|
this,
|
|
resourceProvider,
|
|
);
|
|
contextManager.callbacks = contextManagerCallbacks;
|
|
|
|
analysisDriverSchedulerEventsSubscription = analysisDriverScheduler.events
|
|
.listen(handleAnalysisEvent);
|
|
analysisDriverScheduler.start();
|
|
|
|
onAnalysisStarted.first.then((_) {
|
|
onAnalysisComplete.then((_) {
|
|
performance = performanceAfterStartup = ServerPerformance();
|
|
});
|
|
});
|
|
channel.sendNotification(
|
|
ServerConnectedParams(
|
|
options.reportProtocolVersion ?? PROTOCOL_VERSION,
|
|
io.pid,
|
|
).toNotification(clientUriConverter: uriConverter),
|
|
);
|
|
debounceRequests(
|
|
channel,
|
|
discardedRequests,
|
|
).listen(handleRequestOrResponse, onDone: done, onError: error);
|
|
_newRefactoringManager();
|
|
|
|
pluginManager.initializedCompleter.future.then((_) {
|
|
// Perform "on idle" tasks in case the `pluginManger` determines that no
|
|
// plugins should be run, _after_ the analysis server has reported its
|
|
// final `isAnalyzing: false` status.
|
|
_performOnIdleActions(
|
|
// Use the existing plugin analyzing status.
|
|
isPluginAnalyzing: notificationManager.pluginStatusAnalyzing,
|
|
);
|
|
});
|
|
|
|
notificationManager.pluginAnalysisStatusChanges.listen((
|
|
pluginStatusAnalyzing,
|
|
) {
|
|
if (!pluginManager.initializedCompleter.isCompleted) {
|
|
// Without `this.`, some portion of the analyzer believes we are
|
|
// accessing the super parameter, instead of the field in the super
|
|
// class. See https://github.com/dart-lang/sdk/issues/59996.
|
|
// ignore: unnecessary_this
|
|
this.pluginManager.initializedCompleter.complete();
|
|
} else {
|
|
_performOnIdleActions(isPluginAnalyzing: pluginStatusAnalyzing);
|
|
}
|
|
});
|
|
}
|
|
|
|
/// The most recently registered set of client capabilities. The default is to
|
|
/// have no registered requests and no additional capabilities.
|
|
ServerSetClientCapabilitiesParams get clientCapabilities =>
|
|
_clientCapabilities;
|
|
|
|
/// Updates the current set of client capabilities.
|
|
set clientCapabilities(ServerSetClientCapabilitiesParams capabilities) {
|
|
_clientCapabilities = capabilities;
|
|
|
|
// TODO(dantup): If we can confirm that IntelliJ did not ship code that
|
|
// sets supportsUris=true, then we may be able to entirely remove the
|
|
// uriConverter and all the calls through it.
|
|
if (capabilities.supportsUris ?? false) {
|
|
// URI support implies LSP, as that's the only way to access (and get
|
|
// change notifications for) custom-scheme files.
|
|
uriConverter = ClientUriConverter.withVirtualFileSupport(
|
|
resourceProvider.pathContext,
|
|
);
|
|
initializeLspOverLegacy();
|
|
} else {
|
|
uriConverter = ClientUriConverter.noop(resourceProvider.pathContext);
|
|
}
|
|
|
|
if (capabilities.lspCapabilities
|
|
case Map<Object?, Object?> lspCapabilities) {
|
|
// First validate the capabilities so we can get a better message if it's
|
|
// invalid.
|
|
var reporter = lsp.LspJsonReporter();
|
|
if (!lsp.ClientCapabilities.canParse(lspCapabilities, reporter)) {
|
|
throw RequestError(
|
|
RequestErrorCode.INVALID_PARAMETER,
|
|
"The 'lspCapabilities' parameter was invalid: ${reporter.errors.join(', ')}",
|
|
);
|
|
}
|
|
|
|
_editorClientCapabilities = lsp.LspClientCapabilities(
|
|
lsp.ClientCapabilities.fromJson(lspCapabilities.cast<String, Object>()),
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
lsp.LspClientCapabilities get editorClientCapabilities =>
|
|
_editorClientCapabilities;
|
|
|
|
/// The [Future] that completes when analysis is complete.
|
|
///
|
|
/// This Future is not used for communicating to the client whether we are
|
|
/// analyzing; it is only used by 'search_find' handlers, tests, and for
|
|
/// performance calculations.
|
|
Future<void> get onAnalysisComplete {
|
|
if (_isAnalysisComplete) {
|
|
return Future.value();
|
|
}
|
|
var completer = _onAnalysisCompleteCompleter ??= Completer<void>();
|
|
return completer.future;
|
|
}
|
|
|
|
/// The stream that is notified when the analysis set is changed - this might
|
|
/// be a change to a file, external via a watch event, or internal via
|
|
/// overlay. This means that the resolved world might have changed.
|
|
///
|
|
/// The type of produced elements is not specified and should not be used.
|
|
Stream<void> get onAnalysisSetChanged =>
|
|
_onAnalysisSetChangedController.stream;
|
|
|
|
/// The stream that is notified with `true` when analysis is started.
|
|
Stream<bool> get onAnalysisStarted {
|
|
return _onAnalysisStartedController.stream;
|
|
}
|
|
|
|
@override
|
|
OpenUriNotificationSender? get openUriNotificationSender {
|
|
if (!clientCapabilities.requests.contains('openUrlRequest')) {
|
|
return null;
|
|
}
|
|
|
|
return (Uri uri) async {
|
|
var requestId = '${nextServerRequestId++}';
|
|
await sendRequest(
|
|
ServerOpenUrlRequestParams(
|
|
'$uri',
|
|
).toRequest(requestId, clientUriConverter: uriConverter),
|
|
);
|
|
};
|
|
}
|
|
|
|
RefactoringManager? get refactoringManager {
|
|
var refactoringManager = _refactoringManager;
|
|
if (refactoringManager == null) {
|
|
return null;
|
|
}
|
|
if (refactoringManager.hasPendingRequest) {
|
|
refactoringManager.cancel();
|
|
_newRefactoringManager();
|
|
}
|
|
return _refactoringManager;
|
|
}
|
|
|
|
String get sdkPath {
|
|
return sdkManager.defaultSdkDirectory;
|
|
}
|
|
|
|
@override
|
|
@protected
|
|
bool get supportsShowMessageRequest =>
|
|
clientCapabilities.requests.contains('showMessageRequest');
|
|
|
|
// TODO(srawlins): Do we need to alter this to account for plugin status?
|
|
bool get _isAnalysisComplete => !analysisDriverScheduler.isWorking;
|
|
|
|
void cancelRequest(String id) {
|
|
cancellationTokens[id]?.cancel();
|
|
}
|
|
|
|
/// The socket from which requests are being read has been closed.
|
|
void done() {}
|
|
|
|
/// There was an error related to the socket from which requests are being
|
|
/// read.
|
|
void error(Object argument) {}
|
|
|
|
/// Return the cached analysis result for the file with the given [path].
|
|
/// If there is no cached result, return `null`.
|
|
ResolvedUnitResult? getCachedResolvedUnit(String path) {
|
|
if (!file_paths.isDart(resourceProvider.pathContext, path)) {
|
|
return null;
|
|
}
|
|
|
|
var driver = getAnalysisDriver(path);
|
|
return driver?.getCachedResolvedUnit(path);
|
|
}
|
|
|
|
@override
|
|
FutureOr<void> handleAnalysisStatusChange(analysis.AnalysisStatus status) {
|
|
super.handleAnalysisStatusChange(status);
|
|
sendStatusNotificationNew(status);
|
|
}
|
|
|
|
/// Handle a [request] that was read from the communication channel. The completer
|
|
/// is used to indicate when the request handling is done.
|
|
void handleRequest(
|
|
Request request,
|
|
Completer<void> completer,
|
|
CancelableToken? cancellationToken,
|
|
) {
|
|
var startTime = DateTime.now();
|
|
performance.logRequestTiming(request.clientRequestTime);
|
|
|
|
// Because we don't `await` the execution of the handlers, we wrap the
|
|
// execution in order to have one central place to handle exceptions.
|
|
runZonedGuarded(
|
|
() async {
|
|
// Record performance information for the request.
|
|
var rootPerformance = OperationPerformanceImpl('<root>');
|
|
RequestPerformance? requestPerformance;
|
|
await rootPerformance.runAsync('request', (performance) async {
|
|
requestPerformance = RequestPerformance(
|
|
operation: request.method,
|
|
performance: performance,
|
|
requestLatency: request.timeSinceRequest,
|
|
startTime: startTime,
|
|
);
|
|
recentPerformance.requests.add(requestPerformance!);
|
|
|
|
var token = cancellationToken ??= CancelableToken();
|
|
cancellationTokens[request.id] = token;
|
|
var generator = requestHandlerGenerators[request.method];
|
|
if (generator != null) {
|
|
var handler = generator(this, request, token, performance);
|
|
if (!handler.recordsOwnAnalytics) {
|
|
analyticsManager.startedRequest(
|
|
request: request,
|
|
startTime: startTime,
|
|
);
|
|
}
|
|
await handler.handle();
|
|
} else {
|
|
analyticsManager.startedRequest(
|
|
request: request,
|
|
startTime: startTime,
|
|
);
|
|
sendResponse(Response.unknownRequest(request));
|
|
}
|
|
});
|
|
if (requestPerformance != null &&
|
|
requestPerformance!.performance.elapsed >
|
|
ServerRecentPerformance.slowRequestsThreshold) {
|
|
recentPerformance.slowRequests.add(requestPerformance!);
|
|
}
|
|
completer.complete();
|
|
},
|
|
(exception, stackTrace) {
|
|
if (exception is InconsistentAnalysisException) {
|
|
sendResponse(Response.contentModified(request));
|
|
} else if (exception is RequestFailure) {
|
|
sendResponse(exception.response);
|
|
} else {
|
|
// Log the exception.
|
|
instrumentationService.logException(
|
|
FatalException(
|
|
'Failed to handle request: ${request.method}',
|
|
exception,
|
|
stackTrace,
|
|
),
|
|
null,
|
|
crashReportingAttachmentsBuilder.forException(exception),
|
|
);
|
|
// Then return an error response to the client.
|
|
var error = RequestError(
|
|
RequestErrorCode.SERVER_ERROR,
|
|
exception.toString(),
|
|
);
|
|
error.stackTrace = stackTrace.toString();
|
|
var response = Response(request.id, error: error);
|
|
sendResponse(response);
|
|
}
|
|
completer.complete();
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Handle a [requestOrResponse] that was read from the communication channel.
|
|
void handleRequestOrResponse(RequestOrResponse requestOrResponse) {
|
|
if (requestOrResponse is Request) {
|
|
var cancellationToken = CancelableToken();
|
|
cancellationTokens[requestOrResponse.id] = cancellationToken;
|
|
messageScheduler.add(
|
|
LegacyMessage(
|
|
request: requestOrResponse,
|
|
cancellationToken: cancellationToken,
|
|
),
|
|
);
|
|
} else if (requestOrResponse is Response) {
|
|
handleResponse(requestOrResponse);
|
|
}
|
|
}
|
|
|
|
/// Handle a [response] that was read from the communication channel.
|
|
void handleResponse(Response response) {
|
|
var completer = pendingServerRequests.remove(response.id);
|
|
if (completer != null) {
|
|
completer.complete(response);
|
|
}
|
|
}
|
|
|
|
/// Initializes LSP support over the legacy server.
|
|
///
|
|
/// This method is called when the client sends an LSP request, or indicates
|
|
/// that it will use LSP-overy-Legacy via client capabilities.
|
|
///
|
|
/// This only applies to LSP over the legacy protocol and not DTD, since we
|
|
/// do not want a DTD-LSP client to trigger LSP notifications going to the
|
|
/// legacy protocol client, only the legacy protocol client should do that.
|
|
void initializeLspOverLegacy() {
|
|
sendLspNotifications = true;
|
|
}
|
|
|
|
@override
|
|
void notifyFlutterWidgetDescriptions(String path) {
|
|
flutterWidgetDescriptions.flush();
|
|
}
|
|
|
|
/// Send the given LSP [notification] to the client.
|
|
@override
|
|
void sendLspNotification(lsp.NotificationMessage notification) {
|
|
if (!sendLspNotifications) {
|
|
return;
|
|
}
|
|
|
|
channel.sendNotification(
|
|
LspNotificationParams(
|
|
notification,
|
|
).toNotification(clientUriConverter: uriConverter),
|
|
);
|
|
}
|
|
|
|
/// Sends an LSP request to the server (wrapped in 'lsp.handle') and unwraps
|
|
/// the LSP response from the result of the legacy response.
|
|
@override
|
|
Future<lsp.ResponseMessage> sendLspRequest(
|
|
lsp.Method method,
|
|
Object params,
|
|
) async {
|
|
var id = nextServerRequestId++;
|
|
|
|
// Build the complete LSP RequestMessage to send.
|
|
var lspMessage = lsp.RequestMessage(
|
|
id: lsp.Either2<int, String>.t1(id),
|
|
jsonrpc: lsp.jsonRpcVersion,
|
|
method: method,
|
|
params: params,
|
|
);
|
|
|
|
// Wrap the LSP message inside a call to lsp.handle.
|
|
var response = await sendRequest(
|
|
Request(
|
|
id.toString(),
|
|
lspRequestHandle,
|
|
LspHandleParams(lspMessage).toJson(clientUriConverter: uriConverter),
|
|
),
|
|
);
|
|
|
|
// Unwrap the LSP response from the legacy response.
|
|
var result = LspHandleResult.fromResponse(
|
|
response,
|
|
clientUriConverter: uriConverter,
|
|
);
|
|
var lspResponse = result.lspResponse;
|
|
|
|
return lspResponse is Map<String, Object?>
|
|
? lsp.ResponseMessage.fromJson(lspResponse)
|
|
: lsp.ResponseMessage(
|
|
jsonrpc: lsp.jsonRpcVersion,
|
|
error: lsp.ResponseError(
|
|
code: lsp.ServerErrorCodes.unhandledError,
|
|
message:
|
|
"The client responded to a '$method' LSP request but"
|
|
' did not include a valid response in the lspResponse field',
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Send the given [notification] to the client.
|
|
void sendNotification(Notification notification) {
|
|
channel.sendNotification(notification);
|
|
}
|
|
|
|
/// Send the given [request] to the client.
|
|
Future<Response> sendRequest(Request request) {
|
|
var completer = Completer<Response>();
|
|
pendingServerRequests[request.id] = completer;
|
|
channel.sendRequest(request);
|
|
return completer.future;
|
|
}
|
|
|
|
/// Send the given [response] to the client.
|
|
void sendResponse(Response response) {
|
|
channel.sendResponse(response);
|
|
analyticsManager.sentResponse(response: response);
|
|
cancellationTokens.remove(response.id);
|
|
}
|
|
|
|
/// If the [path] is not a valid file path, that is absolute and normalized,
|
|
/// send an error response, and return `true`. If OK then return `false`.
|
|
bool sendResponseErrorIfInvalidFilePath(Request request, String path) {
|
|
if (!resourceProvider.isAbsoluteAndNormalized(path)) {
|
|
sendResponse(Response.invalidFilePathFormat(request, path));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Sends a `server.error` notification.
|
|
@override
|
|
void sendServerErrorNotification(
|
|
String message,
|
|
Object exception,
|
|
StackTrace? stackTrace, {
|
|
bool fatal = false,
|
|
}) {
|
|
var msg = '$message: $exception';
|
|
if (stackTrace == null && exception is! CaughtException) {
|
|
stackTrace = StackTrace.current;
|
|
}
|
|
|
|
// send the notification
|
|
channel.sendNotification(
|
|
ServerErrorParams(
|
|
fatal,
|
|
msg,
|
|
'$stackTrace',
|
|
).toNotification(clientUriConverter: uriConverter),
|
|
);
|
|
|
|
// remember the last few exceptions
|
|
if (exception is CaughtException) {
|
|
stackTrace ??= exception.stackTrace;
|
|
}
|
|
|
|
exceptions.add(
|
|
ServerException(
|
|
message,
|
|
exception,
|
|
stackTrace is StackTrace ? stackTrace : StackTrace.current,
|
|
fatal,
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Send status notification to the client. The state of analysis is given by
|
|
/// the [status] information.
|
|
void sendStatusNotificationNew(analysis.AnalysisStatus status) {
|
|
var isServerAnalyzing = status.isWorking;
|
|
if (isServerAnalyzing) {
|
|
_onAnalysisStartedController.add(true);
|
|
}
|
|
var onAnalysisCompleteCompleter = _onAnalysisCompleteCompleter;
|
|
if (onAnalysisCompleteCompleter != null && !isServerAnalyzing) {
|
|
onAnalysisCompleteCompleter.complete();
|
|
_onAnalysisCompleteCompleter = null;
|
|
}
|
|
serverStatusAnalyzing = isServerAnalyzing;
|
|
|
|
_performOnIdleActions(
|
|
// Use the existing plugin analyzing status.
|
|
isPluginAnalyzing: notificationManager.pluginStatusAnalyzing,
|
|
);
|
|
}
|
|
|
|
/// Implementation for `analysis.setAnalysisRoots`.
|
|
///
|
|
// TODO(scheglov): implement complete projects/contexts semantics.
|
|
//
|
|
// The current implementation is intentionally simplified and expected
|
|
// that only folders are given each given folder corresponds to the exactly
|
|
// one context.
|
|
//
|
|
// So, we can start working in parallel on adding services and improving
|
|
// projects/contexts support.
|
|
Future<void> setAnalysisRoots(
|
|
String requestId,
|
|
List<String> includedPaths,
|
|
List<String> excludedPaths,
|
|
) async {
|
|
var completer = analysisContextRebuildCompleter = Completer();
|
|
try {
|
|
notificationManager.setAnalysisRoots(includedPaths, excludedPaths);
|
|
try {
|
|
await contextManager.setRoots(includedPaths, excludedPaths);
|
|
} on UnimplementedError catch (e) {
|
|
throw RequestFailure(
|
|
Response.unsupportedFeature(
|
|
requestId,
|
|
e.message ?? 'Unsupported feature.',
|
|
),
|
|
);
|
|
}
|
|
} finally {
|
|
completer.complete();
|
|
}
|
|
}
|
|
|
|
/// Implementation for `analysis.setSubscriptions`.
|
|
void setAnalysisSubscriptions(
|
|
Map<AnalysisService, Set<String>> subscriptions,
|
|
) {
|
|
notificationManager.setSubscriptions(subscriptions);
|
|
analysisServices = subscriptions;
|
|
_sendSubscriptions(analysis: true);
|
|
}
|
|
|
|
/// Implementation for `flutter.setSubscriptions`.
|
|
void setFlutterSubscriptions(Map<FlutterService, Set<String>> subscriptions) {
|
|
flutterServices = subscriptions;
|
|
_sendSubscriptions(flutter: true);
|
|
}
|
|
|
|
/// Implementation for `analysis.setGeneralSubscriptions`.
|
|
void setGeneralAnalysisSubscriptions(
|
|
List<GeneralAnalysisService> subscriptions,
|
|
) {
|
|
var newServices = subscriptions.toSet();
|
|
var newServicesContainsAnalyzedFiles = newServices.contains(
|
|
GeneralAnalysisService.ANALYZED_FILES,
|
|
);
|
|
var generalServicesContainsAnalyzedFiles = generalAnalysisServices.contains(
|
|
GeneralAnalysisService.ANALYZED_FILES,
|
|
);
|
|
if (newServicesContainsAnalyzedFiles &&
|
|
!generalServicesContainsAnalyzedFiles &&
|
|
_isAnalysisComplete) {
|
|
sendAnalysisNotificationAnalyzedFiles(this);
|
|
} else if (!newServicesContainsAnalyzedFiles &&
|
|
generalServicesContainsAnalyzedFiles) {
|
|
prevAnalyzedFiles = null;
|
|
}
|
|
generalAnalysisServices = newServices;
|
|
}
|
|
|
|
/// Set the priority files to the given [files].
|
|
void setPriorityFiles(String requestId, List<String> files) {
|
|
bool isPubspec(String filePath) =>
|
|
file_paths.isPubspecYaml(resourceProvider.pathContext, filePath);
|
|
|
|
// When pubspecs are opened, trigger pre-loading of pub package names and
|
|
// versions.
|
|
var pubspecs = files.where(isPubspec).toList();
|
|
if (pubspecs.isNotEmpty) {
|
|
pubPackageService.beginCachePreloads(pubspecs);
|
|
}
|
|
|
|
priorityFiles.clear();
|
|
priorityFiles.addAll(files);
|
|
// Set priority files in drivers.
|
|
for (var driver in driverMap.values) {
|
|
driver.priorityFiles = files;
|
|
}
|
|
}
|
|
|
|
@override
|
|
@visibleForOverriding
|
|
Future<String?> showUserPrompt(
|
|
MessageType type,
|
|
String message,
|
|
List<String> actionLabels,
|
|
CancellationToken cancellationToken,
|
|
) async {
|
|
assert(supportsShowMessageRequest);
|
|
var requestId = (nextServerRequestId++).toString();
|
|
var actions = actionLabels.map((label) => MessageAction(label)).toList();
|
|
var request = ServerShowMessageRequestParams(
|
|
type.forLegacy,
|
|
message,
|
|
actions,
|
|
).toRequest(requestId, clientUriConverter: uriConverter);
|
|
var responseFuture = sendRequest(request);
|
|
|
|
// Wait for either the result, or cancellation.
|
|
await Future.any([responseFuture, cancellationToken.whenCancelled]);
|
|
|
|
if (cancellationToken.isCancellationRequested) {
|
|
return null;
|
|
} else {
|
|
// If we didn't enter the branch above, we know this future completed.
|
|
var response = await responseFuture;
|
|
return response.result?['action'] as String?;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> shutdown() async {
|
|
await super.shutdown();
|
|
|
|
pubApi.close();
|
|
|
|
detachableFileSystemManager?.dispose();
|
|
|
|
// Defer closing the channel and shutting down the instrumentation server so
|
|
// that the shutdown response can be sent and logged.
|
|
unawaited(
|
|
Future(() {
|
|
instrumentationService.shutdown();
|
|
channel.close();
|
|
}),
|
|
);
|
|
}
|
|
|
|
/// Implementation for `analysis.updateContent`.
|
|
void updateContent(String id, Map<String, Object?> changes) {
|
|
_onAnalysisSetChangedController.add(null);
|
|
changes.forEach((file, change) {
|
|
// Prepare the old overlay contents.
|
|
String? oldContents;
|
|
try {
|
|
if (resourceProvider.hasOverlay(file)) {
|
|
oldContents = resourceProvider.getFile(file).readAsStringSync();
|
|
}
|
|
} catch (_) {}
|
|
|
|
// Prepare the new contents.
|
|
String? newContents;
|
|
int? newVersion;
|
|
if (change is AddContentOverlay) {
|
|
newContents = change.content;
|
|
newVersion = change.version;
|
|
} else if (change is ChangeContentOverlay) {
|
|
if (oldContents == null) {
|
|
// The client may only send a ChangeContentOverlay if there is
|
|
// already an existing overlay for the source.
|
|
throw RequestFailure(
|
|
Response(
|
|
id,
|
|
error: RequestError(
|
|
RequestErrorCode.INVALID_OVERLAY_CHANGE,
|
|
'Invalid overlay change',
|
|
),
|
|
),
|
|
);
|
|
}
|
|
try {
|
|
newContents = SourceEdit.applySequence(oldContents, change.edits);
|
|
newVersion = change.version;
|
|
} on RangeError {
|
|
throw RequestFailure(
|
|
Response(
|
|
id,
|
|
error: RequestError(
|
|
RequestErrorCode.INVALID_OVERLAY_CHANGE,
|
|
'Invalid overlay change',
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} else if (change is RemoveContentOverlay) {
|
|
newContents = null;
|
|
newVersion = null;
|
|
} else {
|
|
// Protocol parsing should have ensured that we never get here.
|
|
throw AnalysisException('Illegal change type');
|
|
}
|
|
|
|
if (newContents != null) {
|
|
resourceProvider.setOverlay(
|
|
file,
|
|
content: newContents,
|
|
modificationStamp: overlayModificationStamp++,
|
|
);
|
|
} else {
|
|
resourceProvider.removeOverlay(file);
|
|
}
|
|
if (newVersion != null) {
|
|
documentVersions[file] = newVersion;
|
|
} else {
|
|
documentVersions.remove(file);
|
|
}
|
|
|
|
for (var driver in driverMap.values) {
|
|
driver.changeFile(file);
|
|
}
|
|
|
|
// If the file did not exist, and is "overlay only", it still should be
|
|
// analyzed. Add it to driver to which it should have been added.
|
|
contextManager.getDriverFor(file)?.addFile(file);
|
|
|
|
notifyFlutterWidgetDescriptions(file);
|
|
|
|
// TODO(scheglov): implement other cases
|
|
});
|
|
}
|
|
|
|
/// Use the given updaters to update the values of the options in every
|
|
/// existing analysis context.
|
|
void updateOptions(List<OptionUpdater> optionUpdaters) {
|
|
// TODO(scheglov): implement for the new analysis driver
|
|
// //
|
|
// // Update existing contexts.
|
|
// //
|
|
// for (AnalysisContext context in analysisContexts) {
|
|
// AnalysisOptionsImpl options =
|
|
// new AnalysisOptionsImpl.from(context.analysisOptions);
|
|
// optionUpdaters.forEach((OptionUpdater optionUpdater) {
|
|
// optionUpdater(options);
|
|
// });
|
|
// context.analysisOptions = options;
|
|
// // `TODO`(brianwilkerson) As far as I can tell, this doesn't cause analysis
|
|
// // to be scheduled for this context.
|
|
// }
|
|
// //
|
|
// // Update the defaults used to create new contexts.
|
|
// //
|
|
// optionUpdaters.forEach((OptionUpdater optionUpdater) {
|
|
// optionUpdater(defaultContextOptions);
|
|
// });
|
|
}
|
|
|
|
/// Returns `true` if there is a subscription for the given [service] and
|
|
/// [file].
|
|
bool _hasAnalysisServiceSubscription(AnalysisService service, String file) {
|
|
return analysisServices[service]?.contains(file) ?? false;
|
|
}
|
|
|
|
bool _hasFlutterServiceSubscription(FlutterService service, String file) {
|
|
return flutterServices[service]?.contains(file) ?? false;
|
|
}
|
|
|
|
/// Initializes [_refactoringManager] with a new instance.
|
|
void _newRefactoringManager() {
|
|
_refactoringManager = RefactoringManager(this, refactoringWorkspace);
|
|
}
|
|
|
|
/// Performs "on idle" actions, given either a new status for whether the
|
|
/// server is analyzing, or a new status for whether the plugin isolate is
|
|
/// analyzing.
|
|
void _performOnIdleActions({required bool isPluginAnalyzing}) {
|
|
// Perform on-idle actions.
|
|
var isAnalyzing =
|
|
serverStatusAnalyzing ||
|
|
isPluginAnalyzing ||
|
|
!pluginManager.initializedCompleter.isCompleted;
|
|
if (!serverStatusAnalyzing) {
|
|
if (generalAnalysisServices.contains(
|
|
GeneralAnalysisService.ANALYZED_FILES,
|
|
)) {
|
|
sendAnalysisNotificationAnalyzedFiles(this);
|
|
}
|
|
_scheduleAnalysisImplementedNotification();
|
|
filesResolvedSinceLastIdle.clear();
|
|
}
|
|
// Only send status when subscribed.
|
|
if (!serverServices.contains(ServerService.STATUS)) {
|
|
return;
|
|
}
|
|
|
|
// Only send status when it changes.
|
|
if (statusAnalyzing == isAnalyzing) {
|
|
return;
|
|
}
|
|
statusAnalyzing = isAnalyzing;
|
|
if (!serverStatusAnalyzing) {
|
|
// Only send analysis analytics after analysis is complete.
|
|
reportAnalysisAnalytics();
|
|
}
|
|
var analysis = AnalysisStatus(isAnalyzing);
|
|
channel.sendNotification(
|
|
ServerStatusParams(
|
|
analysis: analysis,
|
|
).toNotification(clientUriConverter: uriConverter),
|
|
);
|
|
}
|
|
|
|
void _scheduleAnalysisImplementedNotification() {
|
|
var subscribed = analysisServices[AnalysisService.IMPLEMENTED];
|
|
if (subscribed == null) {
|
|
return;
|
|
}
|
|
|
|
var toSend = subscribed.intersection(filesResolvedSinceLastIdle);
|
|
if (toSend.isEmpty) {
|
|
return;
|
|
}
|
|
|
|
scheduleImplementedNotification(this, toSend);
|
|
}
|
|
|
|
void _sendSubscriptions({bool analysis = false, bool flutter = false}) {
|
|
var files = <String>{};
|
|
|
|
if (analysis) {
|
|
for (var serviceFiles in analysisServices.values) {
|
|
files.addAll(serviceFiles);
|
|
}
|
|
}
|
|
|
|
if (flutter) {
|
|
for (var serviceFiles in flutterServices.values) {
|
|
files.addAll(serviceFiles);
|
|
}
|
|
}
|
|
|
|
for (var file in files) {
|
|
// The result will be produced by the "results" stream with
|
|
// the fully resolved unit, and processed with sending analysis
|
|
// notifications as it happens after content changes.
|
|
if (file_paths.isDart(resourceProvider.pathContext, file)) {
|
|
getResolvedUnit(file, sendCachedToStream: true, interactive: false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class ServerContextManagerCallbacks
|
|
extends CommonServerContextManagerCallbacks {
|
|
@override
|
|
final LegacyAnalysisServer analysisServer;
|
|
|
|
ServerContextManagerCallbacks(this.analysisServer, super.resourceProvider);
|
|
|
|
AbstractNotificationManager get _notificationManager =>
|
|
analysisServer.notificationManager;
|
|
|
|
@override
|
|
void afterContextsCreated() {
|
|
super.afterContextsCreated();
|
|
analysisServer._sendSubscriptions(analysis: true, flutter: true);
|
|
}
|
|
|
|
@override
|
|
void afterWatchEvent(WatchEvent event) {
|
|
analysisServer._onAnalysisSetChangedController.add(null);
|
|
}
|
|
|
|
@override
|
|
void flushResults(List<String> files) {
|
|
sendAnalysisNotificationFlushResults(analysisServer, files);
|
|
}
|
|
|
|
@override
|
|
void handleResolvedUnitResult(ResolvedUnitResult result) {
|
|
var path = result.path;
|
|
|
|
var unit = result.unit;
|
|
if (analysisServer._hasAnalysisServiceSubscription(
|
|
AnalysisService.HIGHLIGHTS,
|
|
path,
|
|
)) {
|
|
_runDelayed(() {
|
|
_notificationManager.recordHighlightRegions(
|
|
NotificationManager.serverId,
|
|
path,
|
|
_computeHighlightRegions(unit),
|
|
);
|
|
});
|
|
}
|
|
if (analysisServer._hasAnalysisServiceSubscription(
|
|
AnalysisService.NAVIGATION,
|
|
path,
|
|
)) {
|
|
_runDelayed(() {
|
|
_notificationManager.recordNavigationParams(
|
|
NotificationManager.serverId,
|
|
path,
|
|
_computeNavigationParams(path, result),
|
|
);
|
|
});
|
|
}
|
|
if (analysisServer._hasAnalysisServiceSubscription(
|
|
AnalysisService.OCCURRENCES,
|
|
path,
|
|
)) {
|
|
_runDelayed(() {
|
|
_notificationManager.recordOccurrences(
|
|
NotificationManager.serverId,
|
|
path,
|
|
_computeOccurrences(unit),
|
|
);
|
|
});
|
|
}
|
|
// if (analysisServer._hasAnalysisServiceSubscription(
|
|
// AnalysisService.OUTLINE, path)) {
|
|
// _runDelayed(() {
|
|
// // `TODO`(brianwilkerson) Change NotificationManager to store params
|
|
// // so that fileKind and libraryName can be recorded / passed along.
|
|
// notificationManager.recordOutlines(NotificationManager.serverId, path,
|
|
// _computeOutlineParams(path, unit, result.lineInfo));
|
|
// });
|
|
// }
|
|
if (analysisServer._hasAnalysisServiceSubscription(
|
|
AnalysisService.CLOSING_LABELS,
|
|
path,
|
|
)) {
|
|
_runDelayed(() {
|
|
sendAnalysisNotificationClosingLabels(
|
|
analysisServer,
|
|
path,
|
|
result.lineInfo,
|
|
unit,
|
|
);
|
|
});
|
|
}
|
|
if (analysisServer._hasAnalysisServiceSubscription(
|
|
AnalysisService.FOLDING,
|
|
path,
|
|
)) {
|
|
_runDelayed(() {
|
|
sendAnalysisNotificationFolding(
|
|
analysisServer,
|
|
path,
|
|
result.lineInfo,
|
|
unit,
|
|
);
|
|
});
|
|
}
|
|
if (analysisServer._hasAnalysisServiceSubscription(
|
|
AnalysisService.OUTLINE,
|
|
path,
|
|
)) {
|
|
_runDelayed(() {
|
|
sendAnalysisNotificationOutline(analysisServer, result);
|
|
});
|
|
}
|
|
if (analysisServer._hasAnalysisServiceSubscription(
|
|
AnalysisService.OVERRIDES,
|
|
path,
|
|
)) {
|
|
_runDelayed(() {
|
|
sendAnalysisNotificationOverrides(analysisServer, path, unit);
|
|
});
|
|
}
|
|
if (analysisServer._hasFlutterServiceSubscription(
|
|
FlutterService.OUTLINE,
|
|
path,
|
|
)) {
|
|
_runDelayed(() {
|
|
sendFlutterNotificationOutline(analysisServer, result);
|
|
});
|
|
}
|
|
}
|
|
|
|
List<HighlightRegion> _computeHighlightRegions(CompilationUnit unit) {
|
|
return DartUnitHighlightsComputer(unit).compute();
|
|
}
|
|
|
|
server.AnalysisNavigationParams _computeNavigationParams(
|
|
String path,
|
|
ParsedUnitResult result,
|
|
) {
|
|
var collector = NavigationCollectorImpl();
|
|
computeDartNavigation(resourceProvider, collector, result, null, null);
|
|
collector.createRegions();
|
|
return server.AnalysisNavigationParams(
|
|
path,
|
|
collector.regions,
|
|
collector.targets,
|
|
collector.files,
|
|
);
|
|
}
|
|
|
|
List<Occurrences> _computeOccurrences(CompilationUnit unit) {
|
|
var collector = OccurrencesCollectorImpl();
|
|
addDartOccurrences(collector, unit);
|
|
return collector.allOccurrences;
|
|
}
|
|
|
|
/// Run [f] in a new [Future].
|
|
///
|
|
/// This method is used to delay sending notifications. If there is a more
|
|
/// important consumer of an analysis results, specifically a code completion
|
|
/// computer, we want it to run before spending time of sending notifications.
|
|
///
|
|
// TODO(scheglov): Consider replacing this with full priority based scheduler.
|
|
//
|
|
// TODO(scheglov): Alternatively, if code completion work in a way that does
|
|
// not produce (at first) fully resolved unit, but only part of it - a single
|
|
// method, or a top-level declaration, we would not have this problem - the
|
|
// completion computer would be the only consumer of the partial analysis
|
|
// result.
|
|
void _runDelayed(void Function() f) {
|
|
Future(f);
|
|
}
|
|
}
|
|
|
|
/// Used to record server exceptions.
|
|
class ServerException {
|
|
final String message;
|
|
final dynamic exception;
|
|
final StackTrace stackTrace;
|
|
final bool fatal;
|
|
|
|
ServerException(this.message, this.exception, this.stackTrace, this.fatal);
|
|
|
|
@override
|
|
String toString() => message;
|
|
}
|
|
|
|
/// A class used by [LegacyAnalysisServer] to record performance information
|
|
/// such as request latency.
|
|
class ServerPerformance {
|
|
/// The creation time and the time when performance information
|
|
/// started to be recorded here.
|
|
final int startTime = DateTime.now().millisecondsSinceEpoch;
|
|
|
|
/// The number of requests.
|
|
int requestCount = 0;
|
|
|
|
/// The number of requests that recorded latency information.
|
|
int latencyCount = 0;
|
|
|
|
/// The total latency (milliseconds) for all recorded requests.
|
|
int requestLatency = 0;
|
|
|
|
/// The maximum latency (milliseconds) for all recorded requests.
|
|
int maxLatency = 0;
|
|
|
|
/// The number of requests with latency > 150 milliseconds.
|
|
int slowRequestCount = 0;
|
|
|
|
/// Log timing information for a request.
|
|
void logRequestTiming(int? clientRequestTime) {
|
|
++requestCount;
|
|
if (clientRequestTime != null) {
|
|
var latency = DateTime.now().millisecondsSinceEpoch - clientRequestTime;
|
|
++latencyCount;
|
|
requestLatency += latency;
|
|
maxLatency = max(maxLatency, latency);
|
|
if (latency > 150) {
|
|
++slowRequestCount;
|
|
}
|
|
}
|
|
}
|
|
}
|