| // 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. | |
| import 'dart:async'; | |
| import 'dart:io' as io; | |
| import 'package:analysis_server/lsp_protocol/protocol.dart' as lsp; | |
| import 'package:analysis_server/src/analytics/analytics_manager.dart'; | |
| import 'package:analysis_server/src/collections.dart'; | |
| import 'package:analysis_server/src/context_manager.dart'; | |
| import 'package:analysis_server/src/legacy_analysis_server.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/error_or.dart' as lsp; | |
| import 'package:analysis_server/src/lsp/handlers/handler_execute_command.dart' | |
| as lsp; | |
| import 'package:analysis_server/src/lsp/handlers/handler_states.dart' as lsp; | |
| import 'package:analysis_server/src/lsp/handlers/handlers.dart' as lsp; | |
| import 'package:analysis_server/src/lsp/lsp_analysis_server.dart' | |
| as lsp | |
| show LspInitializationOptions; | |
| import 'package:analysis_server/src/plugin/notification_manager.dart'; | |
| import 'package:analysis_server/src/plugin/plugin_isolate.dart'; | |
| import 'package:analysis_server/src/plugin/plugin_manager.dart'; | |
| import 'package:analysis_server/src/plugin/plugin_watcher.dart'; | |
| import 'package:analysis_server/src/protocol_server.dart' | |
| as legacy | |
| show MessageType; | |
| import 'package:analysis_server/src/protocol_server.dart' as server; | |
| import 'package:analysis_server/src/scheduler/message_scheduler.dart'; | |
| import 'package:analysis_server/src/scheduler/scheduler_tracking_listener.dart'; | |
| import 'package:analysis_server/src/server/crash_reporting_attachments.dart'; | |
| import 'package:analysis_server/src/server/diagnostic_server.dart'; | |
| import 'package:analysis_server/src/services/completion/completion_performance.dart'; | |
| import 'package:analysis_server/src/services/correction/fix_performance.dart'; | |
| import 'package:analysis_server/src/services/correction/refactoring_performance.dart'; | |
| import 'package:analysis_server/src/services/dart_tooling_daemon/dtd_services.dart'; | |
| import 'package:analysis_server/src/services/perf_witness/perf_witness.dart'; | |
| import 'package:analysis_server/src/services/pub/pub_api.dart'; | |
| import 'package:analysis_server/src/services/pub/pub_command.dart'; | |
| import 'package:analysis_server/src/services/pub/pub_package_service.dart'; | |
| import 'package:analysis_server/src/services/refactoring/legacy/refactoring.dart'; | |
| import 'package:analysis_server/src/services/search/element_visitors.dart'; | |
| import 'package:analysis_server/src/services/search/search_engine.dart'; | |
| import 'package:analysis_server/src/services/search/search_engine_internal.dart'; | |
| import 'package:analysis_server/src/services/user_prompts/dart_fix_prompt_manager.dart'; | |
| import 'package:analysis_server/src/services/user_prompts/survey_manager.dart'; | |
| import 'package:analysis_server/src/services/user_prompts/user_prompts.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/status/utilities/report_data.dart'; | |
| import 'package:analysis_server/src/utilities/extensions/ast.dart'; | |
| import 'package:analysis_server/src/utilities/extensions/object.dart'; | |
| import 'package:analysis_server/src/utilities/file_string_sink.dart'; | |
| import 'package:analysis_server/src/utilities/process.dart'; | |
| import 'package:analysis_server/src/utilities/request_statistics.dart'; | |
| import 'package:analysis_server/src/utilities/tee_string_sink.dart'; | |
| import 'package:analysis_server/src/utilities/timing_byte_store.dart'; | |
| import 'package:analysis_server_plugin/src/correction/assist_performance.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/element/element.dart'; | |
| import 'package:analyzer/exception/exception.dart'; | |
| import 'package:analyzer/file_system/file_system.dart'; | |
| import 'package:analyzer/file_system/overlay_file_system.dart'; | |
| import 'package:analyzer/file_system/physical_file_system.dart'; | |
| import 'package:analyzer/instrumentation/instrumentation.dart'; | |
| import 'package:analyzer/source/line_info.dart'; | |
| import 'package:analyzer/src/dart/analysis/byte_store.dart'; | |
| import 'package:analyzer/src/dart/analysis/driver.dart' as analysis; | |
| import 'package:analyzer/src/dart/analysis/driver.dart'; | |
| import 'package:analyzer/src/dart/analysis/driver_based_analysis_context.dart'; | |
| import 'package:analyzer/src/dart/analysis/file_byte_store.dart' | |
| show EvictingFileByteStore; | |
| import 'package:analyzer/src/dart/analysis/file_content_cache.dart'; | |
| import 'package:analyzer/src/dart/analysis/library_graph.dart'; | |
| import 'package:analyzer/src/dart/analysis/performance_logger.dart'; | |
| import 'package:analyzer/src/dart/analysis/results.dart'; | |
| import 'package:analyzer/src/dart/analysis/session.dart'; | |
| import 'package:analyzer/src/dart/analysis/status.dart' as analysis; | |
| import 'package:analyzer/src/dart/analysis/unlinked_unit_store.dart'; | |
| import 'package:analyzer/src/dartdoc/dartdoc_directive_info.dart'; | |
| 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/util/platform_info.dart'; | |
| import 'package:analyzer/src/utilities/cancellation.dart'; | |
| import 'package:analyzer/src/utilities/extensions/analysis_session.dart'; | |
| import 'package:analyzer/src/workspace/basic.dart'; | |
| import 'package:analyzer/src/workspace/blaze.dart'; | |
| import 'package:analyzer/src/workspace/gn.dart'; | |
| import 'package:analyzer/src/workspace/pub.dart'; | |
| import 'package:analyzer_plugin/protocol/protocol.dart'; | |
| import 'package:analyzer_plugin/src/protocol/protocol_internal.dart' | |
| as analyzer_plugin; | |
| import 'package:analyzer_plugin/src/utilities/client_uri_converter.dart'; | |
| import 'package:collection/collection.dart'; | |
| import 'package:http/http.dart' as http; | |
| import 'package:meta/meta.dart'; | |
| import 'package:path/path.dart' as path; | |
| import 'package:watcher/watcher.dart'; | |
| /// The function for sending `openUri` request to the client. | |
| typedef OpenUriNotificationSender = Future<void> Function(Uri uri); | |
| /// The function for sending prompts to the user and collecting button presses. | |
| typedef UserPromptSender = Future<String?> Function( | |
| MessageType type, | |
| String message, | |
| List<String> actionLabels, | |
| lsp.CancellationToken cancellationToken, | |
| ); | |
| /// Implementations of [AnalysisServer] implement a server that listens | |
| /// on an [AbstractNotificationManager] for analysis messages and process them. | |
| abstract class AnalysisServer { | |
| /// The options of this server instance. | |
| AnalysisServerOptions options; | |
| /// The object through which analytics are to be sent. | |
| final AnalyticsManager analyticsManager; | |
| /// The versions of each document known to the server (keyed by path), used to | |
| /// send back to the client for server-initiated edits so that the client can | |
| /// ensure they have a matching version of the document before applying them. | |
| /// | |
| /// Handlers should prefer to use the `getVersionedDocumentIdentifier` method | |
| /// which will return a null-versioned identifier if the document version is | |
| /// not known. | |
| final Map<String, int> documentVersions = {}; | |
| /// A connection to DTD (the Dart Tooling Daemon) that allows other clients to | |
| /// call server functionality. | |
| /// | |
| /// Initialized by the client through a request that calls [connectToDtd]. | |
| DtdServices? dtd; | |
| /// The object for managing showing surveys to users and recording their | |
| /// responses. | |
| /// | |
| /// `null` if surveys are not enabled. | |
| @visibleForTesting | |
| SurveyManager? surveyManager; | |
| /// The builder for attachments that should be included into crash reports. | |
| final CrashReportingAttachmentsBuilder crashReportingAttachmentsBuilder; | |
| /// The [ContextManager] that handles the mapping from analysis roots to | |
| /// context directories. | |
| late final ContextManager contextManager; | |
| /// The object used to manage sending a subset of notifications to the client. | |
| /// The subset of notifications are those to which plugins may contribute. | |
| AbstractNotificationManager notificationManager; | |
| /// A reference to the handler for executing commands. | |
| /// | |
| /// This allows the server to call commands itself, such as "Fix All in | |
| /// Workspace" triggered from the [DartFixPromptManager]. | |
| lsp.ExecuteCommandHandler? executeCommandHandler; | |
| /// The object used to manage the execution of plugins. | |
| late PluginManager pluginManager; | |
| /// The object used to manage the SDK's known to this server. | |
| final DartSdkManager sdkManager; | |
| /// The [SearchEngine] for this server. | |
| late final SearchEngine searchEngine = SearchEngineImpl(driverMap.values); | |
| /// The optional [ByteStore] to use as [byteStore]. | |
| final ByteStore? providedByteStore; | |
| late ByteStore byteStore; | |
| late FileContentCache fileContentCache; | |
| final UnlinkedUnitStore unlinkedUnitStore = UnlinkedUnitStoreImpl(); | |
| late final analysis.AnalysisDriverScheduler analysisDriverScheduler; | |
| late StreamSubscription<Object?>? analysisDriverSchedulerEventsSubscription; | |
| /// The DiagnosticServer for this AnalysisServer. If available, it can be used | |
| /// to start an http diagnostics server or return the port for an existing | |
| /// server. | |
| final DiagnosticServer? diagnosticServer; | |
| /// Additional diagnostic info from the client editor to include in diagnostic | |
| /// reports. | |
| Object? clientDiagnosticInformation; | |
| /// A [RecentBuffer] of the most recent exceptions encountered by the analysis | |
| /// server. | |
| final RecentBuffer<ServerException> exceptions = RecentBuffer(10); | |
| /// The instrumentation service that is to be used by this analysis server. | |
| InstrumentationService instrumentationService; | |
| /// The session logger that is to be used by this analysis server. | |
| SessionLogger sessionLogger; | |
| /// Performance information after initial analysis is complete | |
| /// or `null` if the initial analysis is not yet complete | |
| ServerPerformance? performanceAfterStartup; | |
| /// A client for making requests to the pub.dev API. | |
| final PubApi pubApi; | |
| /// The performance logger. | |
| final PerformanceLogger? performanceLogger; | |
| /// A service for fetching pub.dev package details. | |
| late final PubPackageService pubPackageService; | |
| /// The class into which performance information is currently being recorded. | |
| /// During startup, this will be the same as [performanceDuringStartup] | |
| /// and after startup is complete, this switches to [performanceAfterStartup]. | |
| late ServerPerformance performance; | |
| /// Performance information before initial analysis is complete. | |
| final ServerPerformance performanceDuringStartup = ServerPerformance(); | |
| /// Performance about recent requests. | |
| final ServerRecentPerformance recentPerformance = ServerRecentPerformance(); | |
| final RequestStatisticsHelper? requestStatistics; | |
| final PerformanceLog<TeeStringSink> analysisPerformanceLogger = | |
| PerformanceLog<TeeStringSink>(TeeStringSink()); | |
| /// Manages prompts telling the user about "dart fix". | |
| late final DartFixPromptManager _dartFixPrompt; | |
| /// The set of the files that are currently priority. | |
| final Set<String> priorityFiles = <String>{}; | |
| /// The [ResourceProvider] using which paths are converted into [Resource]s. | |
| final OverlayResourceProvider resourceProvider; | |
| /// The [ClientUriConverter] to use to convert between URIs/Paths when | |
| /// communicating with the client. | |
| late ClientUriConverter _uriConverter = ClientUriConverter.noop( | |
| resourceProvider.pathContext, | |
| ); | |
| /// The next modification stamp for a changed file in the [resourceProvider]. | |
| /// | |
| /// This value is increased each time it is used and used instead of real | |
| /// modification stamps. It's seeded with `millisecondsSinceEpoch` to reduce | |
| /// the chance of colliding with values used in previous analysis sessions if | |
| /// used as a cache key. | |
| int overlayModificationStamp = DateTime.now().millisecondsSinceEpoch; | |
| /// Indicates whether the next time the server completes analysis is the first | |
| /// one since analysis contexts were built. | |
| /// | |
| /// This is useful for triggering things like [_dartFixPrompt] only when | |
| /// it may be useful to do so (after initial analysis and | |
| /// "pub get"/"pub upgrades"). | |
| bool _isFirstAnalysisSinceContextsBuilt = true; | |
| /// A completer that tracks in-progress analysis context rebuilds. | |
| /// | |
| /// Starts completed and will be replaced with a new completer each time an | |
| /// analysis context rebuild starts. | |
| Completer<void> analysisContextRebuildCompleter = Completer()..complete(); | |
| /// The workspace for rename refactorings. | |
| late final refactoringWorkspace = RefactoringWorkspace( | |
| driverMap.values, | |
| searchEngine, | |
| ); | |
| /// The paths of files for which [ResolvedUnitResult] was produced since | |
| /// the last idle state. | |
| final Set<String> filesResolvedSinceLastIdle = {}; | |
| /// A completer for [lspInitialized]. | |
| final Completer<lsp.InitializedStateMessageHandler> _lspInitializedCompleter = | |
| Completer<lsp.InitializedStateMessageHandler>(); | |
| /// The handler passed to [completeLspInitialization], or `null` if LSP | |
| /// initialization has not completed yet. | |
| /// | |
| /// This allows [lspInitialized] to provide the handler synchronously once it | |
| /// is known. | |
| lsp.InitializedStateMessageHandler? _initializedLspHandler; | |
| /// A completer for [lspUninitialized]. | |
| final Completer<void> _lspUninitializedCompleter = Completer<void>(); | |
| /// A scheduler that keeps track of all incoming messages and schedules them | |
| /// for processing. | |
| final MessageScheduler messageScheduler; | |
| /// A [TimingByteStore] that records timings for reads from the byte store. | |
| TimingByteStore? _timingByteStore; | |
| /// The file byte store created by [createByteStore], if any. | |
| EvictingFileByteStore? _fileByteStore; | |
| /// The memory caching byte store created by [createByteStore], if any. | |
| MemoryCachingByteStore? _memoryCachingByteStore; | |
| /// Whether notifications caused by analysis should be suppressed. | |
| /// | |
| /// This is used when an operation is temporarily modifying overlays and does | |
| /// not want the client to be notified of any analysis happening on the | |
| /// temporary content. | |
| bool suppressAnalysisResults = false; | |
| new( | |
| this.options, | |
| this.sdkManager, | |
| this.diagnosticServer, | |
| this.analyticsManager, | |
| this.crashReportingAttachmentsBuilder, | |
| ResourceProvider baseResourceProvider, | |
| this.instrumentationService, | |
| this.sessionLogger, | |
| http.Client? httpClient, | |
| ProcessRunner? processRunner, | |
| this.notificationManager, { | |
| this.requestStatistics, | |
| // Disable to avoid using this in unit tests. | |
| @visibleForTesting bool enableBlazeWatcher = false, | |
| DartFixPromptManager? dartFixPromptManager, | |
| this.providedByteStore, | |
| PluginManager? pluginManager, | |
| MessageSchedulerListener? messageSchedulerListener, | |
| this.performanceLogger, | |
| required bool usePlugins, | |
| Map<String, String>? environment, | |
| }) : resourceProvider = OverlayResourceProvider(baseResourceProvider), | |
| pubApi = PubApi( | |
| instrumentationService, | |
| sessionLogger, | |
| httpClient, | |
| platform.environment['PUB_HOSTED_URL'], | |
| ), | |
| messageScheduler = MessageScheduler( | |
| listener: | |
| messageSchedulerListener ?? | |
| SchedulerTrackingListener(analyticsManager, performanceLogger), | |
| ) { | |
| messageScheduler.server = this; | |
| // Set the default URI converter. This uses the resource providers path | |
| // context (unlike the initialized value) which allows tests to override it. | |
| uriConverter = ClientUriConverter.noop(baseResourceProvider.pathContext); | |
| // We can only spawn processes (eg. to run pub commands) when backed by | |
| // a real file system, otherwise we may try to run commands in folders that | |
| // don't really exist. If processRunner was supplied, it's likely a mock | |
| // from a test in which case the pub command should still be created. | |
| if (baseResourceProvider is PhysicalResourceProvider) { | |
| processRunner ??= ProcessRunner(environment: environment); | |
| } | |
| var disablePubCommandVariable = | |
| platform.environment[PubCommand.disablePubCommandEnvironmentKey]; | |
| var pubCommand = processRunner != null && disablePubCommandVariable == null | |
| ? PubCommand( | |
| instrumentationService, | |
| sessionLogger, | |
| resourceProvider.pathContext, | |
| processRunner, | |
| ) | |
| : null; | |
| pubPackageService = PubPackageService( | |
| instrumentationService, | |
| sessionLogger, | |
| baseResourceProvider, | |
| pubApi, | |
| pubCommand, | |
| ); | |
| performance = performanceDuringStartup; | |
| this.pluginManager = pluginManager ??= PluginManager( | |
| resourceProvider, | |
| resourceProvider.byteStorePath, | |
| sdkManager.defaultSdkDirectory, | |
| notificationManager, | |
| instrumentationService, | |
| sessionLogger, | |
| processRunner: processRunner ?? ProcessRunner(environment: environment), | |
| ); | |
| var pluginWatcher = PluginWatcher( | |
| resourceProvider, | |
| pluginManager, | |
| pluginsAreEnabled: usePlugins, | |
| ); | |
| var logName = options.newAnalysisDriverLog; | |
| if (logName != null) { | |
| if (logName == 'stdout') { | |
| analysisPerformanceLogger.sink.addSink(io.stdout); | |
| } else if (logName.startsWith('file:')) { | |
| var path = logName.substring('file:'.length); | |
| analysisPerformanceLogger.sink.addSink(FileStringSink(path)); | |
| } | |
| } | |
| var requestStatistics = this.requestStatistics; | |
| if (requestStatistics != null) { | |
| analysisPerformanceLogger.sink.addSink( | |
| requestStatistics.perfLoggerStringSink, | |
| ); | |
| } | |
| byteStore = createByteStore(resourceProvider); | |
| fileContentCache = FileContentCache(resourceProvider); | |
| analysisDriverScheduler = analysis.AnalysisDriverScheduler( | |
| analysisPerformanceLogger, | |
| driverWatcher: pluginWatcher, | |
| ); | |
| contextManager = ContextManagerImpl( | |
| resourceProvider, | |
| sdkManager, | |
| options.packagesFile, | |
| options.enabledExperiments, | |
| byteStore, | |
| fileContentCache, | |
| unlinkedUnitStore, | |
| analysisPerformanceLogger, | |
| analysisDriverScheduler, | |
| instrumentationService, | |
| sessionLogger, | |
| enableBlazeWatcher: enableBlazeWatcher, | |
| withFineDependencies: options.withFineDependencies, | |
| ); | |
| _dartFixPrompt = | |
| dartFixPromptManager ?? | |
| DartFixPromptManager( | |
| this, | |
| UserPromptPreferences( | |
| resourceProvider, | |
| instrumentationService, | |
| sessionLogger, | |
| ), | |
| ); | |
| } | |
| /// A [Future] that completes when any in-progress analysis context rebuild | |
| /// completes. | |
| /// | |
| /// If no context rebuild is in progress, will return an already complete | |
| /// [Future]. | |
| Future<void> get analysisContextsRebuilt => | |
| analysisContextRebuildCompleter.future; | |
| /// Statistics for the byte stores created by [createByteStore], or `null` | |
| /// if the byte store was not created by this server. | |
| AnalysisServerByteStoreStats? get byteStoreStats { | |
| var memoryByteStore = _memoryCachingByteStore; | |
| if (memoryByteStore == null) { | |
| return null; | |
| } | |
| var fileByteStore = _fileByteStore; | |
| return AnalysisServerByteStoreStats( | |
| cacheHitCount: memoryByteStore.cacheHitCount, | |
| cacheMissCount: memoryByteStore.cacheMissCount, | |
| currentSizeBytes: memoryByteStore.currentSizeBytes, | |
| entryCount: memoryByteStore.entryCount, | |
| evictedBytes: memoryByteStore.evictedBytes, | |
| evictedEntryCount: memoryByteStore.evictedEntryCount, | |
| evictionCount: memoryByteStore.evictionCount, | |
| maxSizeBytes: memoryByteStore.maxSizeBytes, | |
| putCount: memoryByteStore.putCount, | |
| storeHitCount: memoryByteStore.storeHitCount, | |
| storeMissCount: memoryByteStore.storeMissCount, | |
| cleanUpCount: fileByteStore?.cleanUpCount, | |
| deletedBytes: fileByteStore?.deletedBytes, | |
| deletedFileCount: fileByteStore?.deletedFileCount, | |
| failedReadCount: fileByteStore?.failedReadCount, | |
| failedWriteCount: fileByteStore?.failedWriteCount, | |
| fileCacheSizeBytes: fileByteStore?.maxSizeBytes, | |
| fileStorePath: fileByteStore?.cachePath, | |
| fileStoreSizeBytes: fileByteStore?.lastKnownSizeBytes, | |
| lastCleanUpTimeMilliseconds: fileByteStore?.lastCleanUpTimeMilliseconds, | |
| lastScannedFileCount: fileByteStore?.lastScannedFileCount, | |
| pendingWriteCount: fileByteStore?.pendingWriteCount, | |
| readCount: fileByteStore?.readCount, | |
| readMissCount: fileByteStore?.readMissCount, | |
| writeBytes: fileByteStore?.writeBytes, | |
| writeCount: fileByteStore?.writeCount, | |
| ); | |
| } | |
| /// A list of timings for the byte store, or `null` if timing is not being | |
| /// tracked. | |
| List<ByteStoreTimings>? get byteStoreTimings => _timingByteStore?.timings; | |
| /// The workspace folders to request resource-scoped configuration for, in | |
| /// addition to the global configuration. | |
| /// | |
| /// Only the LSP server tracks workspace folders, so for other servers this | |
| /// is always empty and only the global configuration is requested. | |
| List<String> get configurationWorkspaceFolders => const []; | |
| /// The list of current analysis sessions in all contexts. | |
| Future<List<AnalysisSessionImpl>> get currentSessions async { | |
| var sessions = <AnalysisSessionImpl>[]; | |
| for (var driver in driverMap.values) { | |
| await driver.applyPendingFileChanges(); | |
| sessions.add(driver.currentSession); | |
| } | |
| return sessions; | |
| } | |
| /// The default line terminator that should be used by the server when there | |
| /// is no existing EOL to copy. | |
| String get defaultEol => platform.lineTerminator; | |
| /// A table mapping [Folder]s to the [AnalysisDriver]s associated with them. | |
| Map<Folder, analysis.AnalysisDriver> get driverMap => | |
| contextManager.driverMap; | |
| /// The capabilities of the editor that owns this server. | |
| /// | |
| /// Request handlers should be careful to use the correct clients capabilities | |
| /// if they are available to non-editor clients (such as over DTD). The | |
| /// capabilities of the caller are available in [lsp.MessageInfo] and should | |
| /// usually be used when computing the results for requests, but if those | |
| /// requests additionally trigger requests to the editor, those requests to | |
| /// the editor should consider these capabilities. | |
| /// | |
| /// For the legacy server, this set may be a fixed set that is not actually | |
| /// configured by the client, but matches what legacy protocol editors expect | |
| /// when using LSP-over-Legacy. | |
| lsp.LspClientCapabilities? get editorClientCapabilities; | |
| /// The initialization options provided by the client for LSP initialization. | |
| /// | |
| /// These are only ever set for the LSP server and always null for | |
| /// LSP-over-Legacy. We now favor using [editorClientCapabilities] | |
| /// for clients to indicate their capabilities and preferences (and | |
| /// [lspClientConfiguration] for the users config/preferences). | |
| lsp.LspInitializationOptions? get initializationOptions; | |
| /// Whether [completeLspInitialization] has been called, and therefore whether | |
| /// [lspInitialized] returns the handler itself instead of a [Future]. | |
| bool get isLspInitialized => _initializedLspHandler != null; | |
| /// The configuration (user/workspace settings) from the LSP client. | |
| /// | |
| /// For the legacy server, this set may be a fixed set that is not controlled | |
| /// by the client. | |
| lsp.LspClientConfiguration get lspClientConfiguration; | |
| /// A [Future] that completes when the LSP server moves into the initialized | |
| /// state and can handle normal LSP requests. | |
| /// | |
| /// Completes with the [lsp.InitializedStateMessageHandler] that is active. | |
| /// Before [completeLspInitialization] has been called this is a [Future], | |
| /// afterwards it is the handler itself, so callers that are not already async | |
| /// can avoid an `await`. | |
| /// | |
| /// When the server leaves the initialized state, [lspUninitialized] will | |
| /// complete. | |
| FutureOr<lsp.InitializedStateMessageHandler> get lspInitialized => | |
| _initializedLspHandler ?? _lspInitializedCompleter.future; | |
| /// A [Future] that completes once the server transitions out of an | |
| /// initialized state. | |
| Future<void> get lspUninitialized => _lspUninitializedCompleter.future; | |
| /// Returns the function that can send `openUri` request to the client. | |
| /// Returns `null` is the client does not support it. | |
| OpenUriNotificationSender? get openUriNotificationSender; | |
| /// Returns owners of files. | |
| OwnedFiles get ownedFiles => contextManager.ownedFiles; | |
| path.Context get pathContext => resourceProvider.pathContext; | |
| /// Whether or not the client supports showMessageRequest to show the user | |
| /// a message and allow them to respond by clicking buttons. | |
| /// | |
| /// Callers should use [userPromptSender] instead of checking this directly. | |
| @protected | |
| bool get supportsShowMessageRequest; | |
| /// Return the total time the server's been alive. | |
| Duration get uptime { | |
| var start = DateTime.fromMillisecondsSinceEpoch( | |
| performanceDuringStartup.startTime, | |
| ); | |
| return DateTime.now().difference(start); | |
| } | |
| /// The [ClientUriConverter] to use to convert between URIs/Paths when | |
| /// communicating with the client. | |
| ClientUriConverter get uriConverter => _uriConverter; | |
| /// Sets the [ClientUriConverter] to use to convert between URIs/Paths when | |
| /// communicating with the client. | |
| set uriConverter(ClientUriConverter value) { | |
| notificationManager.uriConverter = _uriConverter = value; | |
| } | |
| /// Returns the function for sending prompts to the user and collecting button | |
| /// presses. | |
| /// | |
| /// Returns `null` is the client does not support it. | |
| UserPromptSender? get userPromptSender => | |
| supportsShowMessageRequest ? showUserPrompt : null; | |
| void afterContextsCreated() { | |
| _timingByteStore?.newTimings('after contexts created'); | |
| _isFirstAnalysisSinceContextsBuilt = true; | |
| } | |
| void afterContextsDestroyed() {} | |
| /// Broadcast a request built from the given [requestParams] to all of the | |
| /// plugins that are currently associated with the context root from the given | |
| /// [driver]. Return a list containing futures that will complete when each of | |
| /// the plugins have sent a response, or an empty list if no [driver] is | |
| /// provided. | |
| Map<PluginIsolate, Future<Response>> broadcastRequestToPlugins( | |
| analyzer_plugin.RequestParams requestParams, | |
| analysis.AnalysisDriver? driver, | |
| ) { | |
| if (driver == null) { | |
| return <PluginIsolate, Future<Response>>{}; | |
| } | |
| return pluginManager.broadcastRequest( | |
| requestParams, | |
| contextRoot: driver.analysisContext!.contextRoot, | |
| ); | |
| } | |
| /// Display a message that will allow us to enable analytics on the next run. | |
| void checkAnalytics() { | |
| var unifiedAnalytics = analyticsManager.analytics; | |
| var prompt = userPromptSender; | |
| if (!unifiedAnalytics.shouldShowMessage || prompt == null) { | |
| return; | |
| } | |
| unawaited( | |
| prompt(MessageType.info, unifiedAnalytics.getConsentMessage, [ | |
| lsp.UserPromptActions.ok, | |
| ], NotCancelableToken()), | |
| ); | |
| unifiedAnalytics.clientShowedMessage(); | |
| } | |
| /// Checks that all [sessions] are still consistent, throwing | |
| /// [InconsistentAnalysisException] if not. | |
| void checkConsistency(List<AnalysisSessionImpl> sessions) { | |
| for (var session in sessions) { | |
| session.checkConsistency(); | |
| } | |
| } | |
| /// Completes [lspInitialized], signalling that the server has moved into an | |
| /// initialized state where it can handle standard LSP requests. | |
| /// | |
| /// This must be called exactly once; a second call throws. The legacy server | |
| /// can have initialization completed from more than one code path and routes | |
| /// them through `LegacyAnalysisServer.completeLspInitializationIfNeeded`. | |
| void completeLspInitialization(lsp.InitializedStateMessageHandler handler) { | |
| _initializedLspHandler = handler; | |
| _lspInitializedCompleter.complete(handler); | |
| } | |
| /// Completes [lspUninitialized], signalling that the server has moved out | |
| /// of a state where it can handle standard LSP requests. | |
| void completeLspUninitialization() { | |
| if (!_lspUninitializedCompleter.isCompleted) { | |
| _lspUninitializedCompleter.complete(); | |
| } | |
| } | |
| /// Connects to DTD at [dtdUri]. | |
| /// | |
| /// If there is already an active connection to DTD or there is an error | |
| /// connecting, returns an error, otherwise returns `null`. | |
| Future<lsp.ErrorOr<Null>> connectToDtd( | |
| Uri dtdUri, { | |
| bool registerExperimentalHandlers = false, | |
| }) async { | |
| switch (dtd?.state) { | |
| case DtdConnectionState.connecting || DtdConnectionState.connected: | |
| return lsp.error( | |
| lsp.ServerErrorCodes.stateError, | |
| 'Server is already connected to DTD', | |
| ); | |
| case DtdConnectionState.disconnected || DtdConnectionState.error || null: | |
| var connectResult = await DtdServices.connect( | |
| this, | |
| dtdUri, | |
| registerExperimentalHandlers: registerExperimentalHandlers, | |
| ); | |
| return connectResult.mapResultSync((dtd) { | |
| this.dtd = dtd; | |
| return lsp.success(null); | |
| }); | |
| } | |
| } | |
| /// If the state location can be accessed, return the file byte store, | |
| /// otherwise return the memory byte store. | |
| ByteStore createByteStore(ResourceProvider resourceProvider) { | |
| const M = 1024 * 1024 /*1 MiB*/; | |
| const G = 1024 * 1024 * 1024 /*1 GiB*/; | |
| const fileCacheSize = 4 * G; | |
| const memoryCacheSize = 256 * M; | |
| if (providedByteStore case var providedByteStore?) { | |
| if (providedByteStore is MemoryCachingByteStore) { | |
| _memoryCachingByteStore = providedByteStore; | |
| } | |
| return providedByteStore; | |
| } | |
| if (options.disableFileByteStore ?? false) { | |
| return _memoryCachingByteStore = MemoryCachingByteStore( | |
| NullByteStore(), | |
| memoryCacheSize, | |
| ); | |
| } | |
| if (resourceProvider is OverlayResourceProvider) { | |
| resourceProvider = resourceProvider.baseProvider; | |
| } | |
| if (resourceProvider is PhysicalResourceProvider) { | |
| var stateLocation = resourceProvider.getStateLocation('.analysis-driver'); | |
| if (stateLocation != null) { | |
| var fileByteStore = _fileByteStore = EvictingFileByteStore( | |
| stateLocation.path, | |
| fileCacheSize, | |
| ); | |
| var timingByteStore = _timingByteStore = TimingByteStore(fileByteStore); | |
| return _memoryCachingByteStore = MemoryCachingByteStore( | |
| timingByteStore, | |
| memoryCacheSize, | |
| ); | |
| } | |
| } | |
| return _memoryCachingByteStore = MemoryCachingByteStore( | |
| NullByteStore(), | |
| memoryCacheSize, | |
| ); | |
| } | |
| void enableSurveys() { | |
| // TODO(dantup): This is currently gated on an LSP flag and should just | |
| // be inlined into the constructor once we're happy for it to show up | |
| // without a flag and for both protocols. | |
| surveyManager = SurveyManager( | |
| this, | |
| instrumentationService, | |
| sessionLogger, | |
| analyticsManager.analytics, | |
| ); | |
| } | |
| /// Fetches the configuration from the client (if supported) and updates | |
| /// [lspClientConfiguration] with it. | |
| /// | |
| /// Configuration is requested for the whole workspace and additionally for | |
| /// each of [configurationWorkspaceFolders]. | |
| Future<void> fetchClientConfiguration() async { | |
| if (!(editorClientCapabilities?.configuration ?? false)) { | |
| return; | |
| } | |
| // Take a copy of workspace folders because we need to match up the | |
| // responses to the request by index and it's possible the folders will | |
| // change after we sent the request but before we get the response. | |
| var folders = configurationWorkspaceFolders.toList(); | |
| // Fetch all configuration we care about from the client. This is just | |
| // "dart" for now, but in future this may be extended to include | |
| // others (for example "flutter"). | |
| var response = await sendLspRequest( | |
| lsp.Method.workspace_configuration, | |
| lsp.ConfigurationParams( | |
| items: [ | |
| // Dart settings for each workspace folder. | |
| for (var folder in folders) | |
| lsp.ConfigurationItem( | |
| scopeUri: uriConverter.toClientUri(folder), | |
| section: 'dart', | |
| ), | |
| // Global Dart settings. This comes last to simplify matching up the | |
| // indexes in the results (folder[i] is the i'th item). | |
| lsp.ConfigurationItem(section: 'dart'), | |
| ], | |
| ), | |
| ); | |
| var result = response.result; | |
| // Expect the result to be a list with 1 + folders.length items to | |
| // match the request above, and each should be a standard map of settings. | |
| // If the above code is extended to support multiple sets of config | |
| // this will need tweaking to handle the item for each section. | |
| if (result != null && | |
| result is List<Object?> && | |
| result.length == 1 + folders.length) { | |
| // Config is stored as a map keyed by the workspace folder, and a key of | |
| // null for the global config | |
| var workspaceFolderConfig = { | |
| for (var i = 0; i < folders.length; i++) | |
| folders[i]: result[i] as Map<String, Object?>? ?? {}, | |
| }; | |
| var newGlobalConfig = result.last as Map<String, Object?>? ?? {}; | |
| var oldGlobalConfig = lspClientConfiguration.global; | |
| lspClientConfiguration.replace(newGlobalConfig, workspaceFolderConfig); | |
| // Refreshing the analysis roots also re-analyzes, so only one of these is | |
| // needed. Where [refreshAnalysisRoots] does nothing the settings that | |
| // affect the roots (such as excluded folders) have no effect either, so | |
| // skipping the re-analysis only matters for a change that affects both. | |
| if (lspClientConfiguration.affectsAnalysisRoots(oldGlobalConfig)) { | |
| await refreshAnalysisRoots(); | |
| } else if (lspClientConfiguration.affectsAnalysisResults( | |
| oldGlobalConfig, | |
| )) { | |
| // Some settings affect analysis results and require re-analysis | |
| // (such as showTodos). | |
| await reanalyze(); | |
| } | |
| } | |
| } | |
| /// Return an analysis driver to which the file with the given [path] is | |
| /// added if one exists, otherwise a driver in which the file was analyzed if | |
| /// one exists, otherwise the first driver, otherwise `null`. | |
| analysis.AnalysisDriver? getAnalysisDriver(String path) { | |
| var drivers = driverMap.values.toList(); | |
| if (drivers.isNotEmpty) { | |
| // Sort the drivers so that more deeply nested contexts will be checked | |
| // before enclosing contexts. | |
| drivers.sort((first, second) { | |
| var firstRoot = first.analysisContext!.contextRoot.root.path; | |
| var secondRoot = second.analysisContext!.contextRoot.root.path; | |
| return secondRoot.length - firstRoot.length; | |
| }); | |
| var driver = drivers.firstWhereOrNull( | |
| (driver) => driver.analysisContext!.contextRoot.isAnalyzed(path), | |
| ); | |
| driver ??= drivers.firstWhereOrNull( | |
| (driver) => driver.fsState.getExistingFromPath(path) != null, | |
| ); | |
| driver ??= drivers.first; | |
| return driver; | |
| } | |
| return null; | |
| } | |
| /// Return an [AnalysisSession] in which the file with the given [path] | |
| /// should be analyzed, preferring the one in which it is analyzed, then | |
| /// the one where it is referenced, then the first, otherwise `null`. | |
| Future<AnalysisSession?> getAnalysisSession(String path) async { | |
| var analysisDriver = getAnalysisDriver(path); | |
| if (analysisDriver != null) { | |
| await analysisDriver.applyPendingFileChanges(); | |
| return analysisDriver.currentSession; | |
| } | |
| return null; | |
| } | |
| DartdocDirectiveInfo getDartdocDirectiveInfoFor(ResolvedUnitResult result) { | |
| return getDartdocDirectiveInfoForSession(result.session); | |
| } | |
| DartdocDirectiveInfo getDartdocDirectiveInfoForSession( | |
| AnalysisSession session, | |
| ) { | |
| var analysisContext = session.analysisContext; | |
| analysisContext as DriverBasedAnalysisContext; | |
| return analysisContext.driver.dartdocDirectiveInfo; | |
| } | |
| /// Gets the current version number of a document. | |
| int? getDocumentVersion(String path) => documentVersions[path]; | |
| /// Return a [Future] that completes with the [Element] at the given | |
| /// [offset] of the given [file], or with `null` if there is no node at the | |
| /// [offset] or the node does not have an element. | |
| Future<Element?> getElementAtOffset(String file, int offset) async { | |
| var unitResult = await getResolvedUnit(file); | |
| if (unitResult == null) { | |
| return null; | |
| } | |
| if (!priorityFiles.contains(file)) { | |
| var fragment = findFragmentByNameOffset( | |
| unitResult.libraryFragment, | |
| offset, | |
| ); | |
| if (fragment != null) { | |
| return fragment.element; | |
| } | |
| } | |
| var node = unitResult.unit.nodeCovering(offset: offset); | |
| return node?.getElement(); | |
| } | |
| /// Return a [LineInfo] for the file with the given [path]. | |
| /// | |
| /// If the file does not exist or cannot be read, returns `null`. | |
| /// | |
| /// This method supports non-Dart files but uses the current content of the | |
| /// file which may not be the latest analyzed version of the file if it was | |
| /// recently modified, so using the LineInfo from an analyzed result may be | |
| /// preferable. This method exists mainly to support plugins which do not | |
| /// provide us a matching [LineInfo] for the content they used. | |
| LineInfo? getLineInfo(String path) { | |
| try { | |
| // First try from the overlay because it may be more up-to-date than | |
| // the file state. | |
| var content = resourceProvider.getFile(path).readAsStringSync(); | |
| return LineInfo.fromContent(content); | |
| } on FileSystemException { | |
| // If the file does not exist or cannot be read, try the file state | |
| // because this could be something like a macro-generated file. | |
| var result = getAnalysisDriver(path)?.getFileSync(path); | |
| if (result is FileResult) { | |
| return result.lineInfo; | |
| } | |
| return null; | |
| } | |
| } | |
| /// Return the unresolved unit for the file with the given [path]. | |
| /// | |
| /// Callers should handle [InconsistentAnalysisException] exceptions that may | |
| /// occur if a file is modified during this operation. | |
| Future<ParsedUnitResult?> getParsedUnit(String path) async { | |
| if (!file_paths.isDart(resourceProvider.pathContext, path)) { | |
| return null; | |
| } | |
| var session = await getAnalysisSession(path); | |
| if (session == null) { | |
| return null; | |
| } | |
| var result = session.getParsedUnit(path); | |
| return result is ParsedUnitResult ? result : null; | |
| } | |
| /// Return the resolved library for the library containing the file with the | |
| /// given [path]. The library is analyzed in one of the analysis drivers to | |
| /// which the file was added, otherwise in the first driver, otherwise `null` | |
| /// is returned. | |
| Future<ResolvedLibraryResult?> getResolvedLibrary(String path) async { | |
| if (!file_paths.isDart(resourceProvider.pathContext, path)) { | |
| return null; | |
| } | |
| var driver = getAnalysisDriver(path); | |
| if (driver == null) { | |
| return null; | |
| } | |
| try { | |
| return await driver.currentSession.getResolvedContainingLibrary(path); | |
| } on InconsistentAnalysisException { | |
| return null; | |
| } catch (exception, stackTrace) { | |
| instrumentationService.logException(exception, stackTrace); | |
| sessionLogger.logException(exception: exception, stackTrace: stackTrace); | |
| } | |
| return null; | |
| } | |
| /// Return the resolved unit for the file with the given [path]. The file is | |
| /// analyzed in one of the analysis drivers to which the file was added, | |
| /// otherwise in the first driver, otherwise `null` is returned. | |
| Future<ResolvedUnitResult?>? getResolvedUnit( | |
| String path, { | |
| bool sendCachedToStream = false, | |
| bool interactive = true, | |
| }) { | |
| if (!file_paths.isDart(resourceProvider.pathContext, path)) { | |
| return null; | |
| } | |
| var driver = getAnalysisDriver(path); | |
| if (driver == null) { | |
| return Future.value(); | |
| } | |
| return driver | |
| .getResolvedUnit( | |
| path, | |
| sendCachedToStream: sendCachedToStream, | |
| interactive: interactive, | |
| ) | |
| .then((value) => value is ResolvedUnitResult ? value : null) | |
| .catchError((Object exception, StackTrace stackTrace) { | |
| instrumentationService.logException(exception, stackTrace); | |
| sessionLogger.logException( | |
| exception: exception, | |
| stackTrace: stackTrace, | |
| ); | |
| return null; | |
| }); | |
| } | |
| /// Gets the version of a document known to the server, returning a | |
| /// [lsp.OptionalVersionedTextDocumentIdentifier] with a version of `null` if the | |
| /// document version is not known. | |
| lsp.OptionalVersionedTextDocumentIdentifier getVersionedDocumentIdentifier( | |
| String path, | |
| ) { | |
| return lsp.OptionalVersionedTextDocumentIdentifier( | |
| uri: uriConverter.toClientUri(path), | |
| version: getDocumentVersion(path), | |
| ); | |
| } | |
| @mustCallSuper | |
| void handleAnalysisEvent(Object event) { | |
| switch (event) { | |
| case FileResult(): | |
| contextManager.callbacks.handleFileResult(event); | |
| case analysis.AnalysisStatus(): | |
| handleAnalysisStatusChange(event); | |
| } | |
| } | |
| @mustCallSuper | |
| FutureOr<void> handleAnalysisStatusChange(analysis.AnalysisStatus status) { | |
| if (_isFirstAnalysisSinceContextsBuilt && !status.isWorking) { | |
| _timingByteStore?.newTimings('initial analysis completed'); | |
| _isFirstAnalysisSinceContextsBuilt = false; | |
| _dartFixPrompt.triggerCheck(); | |
| } | |
| analyticsManager.analysisStatusChanged( | |
| isWorking: status.isWorking, | |
| statistics: status | |
| .ifTypeOrNull<analysis.AnalysisStatusIdle>() | |
| ?.workingStatistics, | |
| ); | |
| } | |
| /// Immediately handles an LSP message by delegating to the | |
| /// [lsp.InitializedStateMessageHandler]. This method does not schedule the | |
| /// message and is intended to be used by the scheduler (or LSP-over-Legacy | |
| /// where the original legacy wrapper was already scheduled). | |
| /// | |
| /// If the LSP server/support is not yet initialized, will wait until it is. | |
| FutureOr<lsp.ErrorOr<Object?>> immediatelyHandleLspMessage( | |
| lsp.IncomingMessage message, | |
| lsp.MessageInfo messageInfo, { | |
| lsp.CancellationToken? cancellationToken, | |
| }) async { | |
| // `lspInitialized` returns the handler itself once | |
| // `completeLspInitialization` has run and only before that a future, so the | |
| // await can usually be skipped. | |
| var initializedLspHandler = lspInitialized; | |
| var handler = initializedLspHandler is lsp.InitializedStateMessageHandler | |
| ? initializedLspHandler | |
| : await initializedLspHandler; | |
| return handler.handleMessage( | |
| message, | |
| messageInfo, | |
| cancellationToken: cancellationToken, | |
| ); | |
| } | |
| /// Return `true` if the file or directory with the given [path] will be | |
| /// analyzed in one of the analysis contexts. | |
| bool isAnalyzed(String path) { | |
| return contextManager.isAnalyzed(path); | |
| } | |
| void logExceptionResult(analysis.ExceptionResult result) { | |
| var message = 'Analysis failed: ${result.filePath}'; | |
| if (result.contextKey != null) { | |
| message += ' context: ${result.contextKey}'; | |
| } | |
| var attachments = crashReportingAttachmentsBuilder.forExceptionResult( | |
| result, | |
| ); | |
| // TODO(39284): should this exception be silent? | |
| instrumentationService.logException( | |
| SilentException.wrapInMessage(message, result.exception), | |
| null, | |
| attachments, | |
| ); | |
| sessionLogger.logException( | |
| exception: SilentException.wrapInMessage(message, result.exception), | |
| attachments: attachments, | |
| ); | |
| } | |
| /// Notify the flutter widget properties support that the file with the | |
| /// given [path] was changed - added, updated, or removed. | |
| void notifyFlutterWidgetDescriptions(String path) {} | |
| /// Prevents the scheduler from processing new messages until [operation] | |
| /// completes. | |
| /// | |
| /// This can be used to obtain analysis results/resolved units consistent with | |
| /// the state of a file at the time this method was called, preventing | |
| /// changes by incoming file modifications. | |
| /// | |
| /// The contents of [operation] should be kept as short as possible and since | |
| /// cancellation requests will also be blocked for the duration of this | |
| /// operation, handlers should generally check the cancellation flag | |
| /// immediately after this function returns. | |
| Future<T> pauseSchedulerWhile<T>(FutureOr<T> Function() operation) async { | |
| // TODO(dantup): Prevent this method from locking responses from the client | |
| // because this can lead to deadlocks if called during initialization where | |
| // the server may wait for something (configuration) from the client. This | |
| // might fit in with potential upcoming scheduler changes. | |
| // | |
| // This is currently used by Completion+FixAll (which are less likely, but | |
| // possible to be called during init). | |
| // | |
| // https://github.com/dart-lang/sdk/issues/56311#issuecomment-2250089185 | |
| messageScheduler.pause(); | |
| try { | |
| // `await` here is important to ensure `finally` doesn't execute until | |
| // `operation()` completes (`whenComplete` is not available on | |
| // `FutureOr`). | |
| return await operation(); | |
| } finally { | |
| messageScheduler.resume(); | |
| } | |
| } | |
| void publishLspClosingLabels(String path, List<lsp.ClosingLabel> labels) { | |
| var params = lsp.PublishClosingLabelsParams( | |
| uri: uriConverter.toClientUri(path), | |
| labels: labels, | |
| ); | |
| var message = lsp.NotificationMessage( | |
| method: lsp.CustomMethods.publishClosingLabels, | |
| params: params, | |
| jsonrpc: lsp.jsonRpcVersion, | |
| ); | |
| sendLspNotification(message); | |
| } | |
| /// Read all files, resolve all URIs, and perform required analysis in | |
| /// all current analysis drivers. | |
| Future<void> reanalyze() async { | |
| await contextManager.refresh(); | |
| } | |
| /// Recomputes the analysis roots because client configuration that affects | |
| /// them (such as excluded folders) has changed. | |
| /// | |
| /// Analysis roots are only computed from client configuration by the LSP | |
| /// server. Other clients set their roots explicitly (for the legacy protocol | |
| /// with `analysis.setAnalysisRoots`) so there is nothing to do here. | |
| @protected | |
| Future<void> refreshAnalysisRoots() async {} | |
| /// Report analytics data related to the number and size of files that were | |
| /// analyzed. | |
| void reportAnalysisAnalytics() { | |
| if (performanceLogger != null) { | |
| Map<String, Object?> collectedData = {}; | |
| collectContextData(collectedData, this); | |
| performanceLogger!.logMap(collectedData); | |
| } | |
| if (!analyticsManager.needsAnslysisCompleteCall()) { | |
| return; | |
| } | |
| var packagesFileMap = <String, File?>{}; | |
| var immediateFileCount = 0; | |
| var immediateFileLineCount = 0; | |
| var transitiveFileCount = 0; | |
| var transitiveFileLineCount = 0; | |
| var transitiveFilePaths = <String>{}; | |
| var transitiveFileUniqueLineCount = 0; | |
| var libraryCycles = <LibraryCycle>{}; | |
| var workspaceTypes = List<int>.filled(5, 0); | |
| var numberOfPackages = <int>[]; | |
| var driverMap = contextManager.driverMap; | |
| for (var entry in driverMap.entries) { | |
| var rootPath = entry.key.path; | |
| var driver = entry.value; | |
| var analysisContext = driver.analysisContext; | |
| if (analysisContext != null) { | |
| var contextRoot = analysisContext.contextRoot; | |
| packagesFileMap[rootPath] = contextRoot.packagesFile; | |
| var workspace = contextRoot.workspace; | |
| switch (workspace) { | |
| case PackageConfigWorkspace(): | |
| if (workspace.isPubWorkspace) { | |
| // Pub workspace index = 2, Package workspace index = 1 | |
| workspaceTypes[2]++; | |
| numberOfPackages.add(workspace.allPackages.length); | |
| } else { | |
| workspaceTypes[1]++; | |
| } | |
| case BlazeWorkspace(): | |
| workspaceTypes[3]++; | |
| case GnWorkspace(): | |
| workspaceTypes[4]++; | |
| case BasicWorkspace(): | |
| workspaceTypes[0]++; | |
| } | |
| } | |
| var fileSystemState = driver.fsState; | |
| // Capture the known files before the loop to prevent a concurrent | |
| // modification exception. The reason for the exception is unknown. | |
| var knownFiles = fileSystemState.knownFiles.toList(); | |
| for (var fileState in knownFiles) { | |
| var isImmediate = fileState.path.startsWith(rootPath); | |
| if (isImmediate) { | |
| immediateFileCount++; | |
| immediateFileLineCount += fileState.lineInfo.lineCount; | |
| var libraryKind = fileState.kind.library; | |
| if (libraryKind != null) { | |
| libraryCycles.add(libraryKind.libraryCycle); | |
| } | |
| } else { | |
| var lineCount = fileState.lineInfo.lineCount; | |
| transitiveFileCount++; | |
| transitiveFileLineCount += lineCount; | |
| if (transitiveFilePaths.add(fileState.path)) { | |
| transitiveFileUniqueLineCount += lineCount; | |
| } | |
| } | |
| } | |
| } | |
| var transitiveFileUniqueCount = transitiveFilePaths.length; | |
| var libraryCycleLibraryCounts = <int>[]; | |
| var libraryCycleLineCounts = <int>[]; | |
| for (var libraryCycle in libraryCycles) { | |
| var libraries = libraryCycle.libraries; | |
| var lineCount = 0; | |
| for (var library in libraries) { | |
| for (var file in library.files) { | |
| lineCount += file.lineInfo.lineCount; | |
| } | |
| } | |
| libraryCycleLibraryCounts.add(libraries.length); | |
| libraryCycleLineCounts.add(lineCount); | |
| } | |
| analyticsManager.analysisComplete( | |
| numberOfContexts: driverMap.length, | |
| immediateFileCount: immediateFileCount, | |
| immediateFileLineCount: immediateFileLineCount, | |
| transitiveFileCount: transitiveFileCount, | |
| transitiveFileLineCount: transitiveFileLineCount, | |
| transitiveFileUniqueCount: transitiveFileUniqueCount, | |
| transitiveFileUniqueLineCount: transitiveFileUniqueLineCount, | |
| libraryCycleLibraryCounts: libraryCycleLibraryCounts, | |
| libraryCycleLineCounts: libraryCycleLineCounts, | |
| contextWorkspaceType: workspaceTypes, | |
| numberOfPackagesInWorkspace: numberOfPackages, | |
| ); | |
| } | |
| Future<ResolvedForCompletionResultImpl?> resolveForCompletion({ | |
| required String path, | |
| required int offset, | |
| required OperationPerformanceImpl performance, | |
| }) async { | |
| if (!file_paths.isDart(resourceProvider.pathContext, path)) { | |
| return null; | |
| } | |
| var driver = getAnalysisDriver(path); | |
| if (driver == null) { | |
| return null; | |
| } | |
| var result = _tryGetCachedResolvedUnitAsResolvedForCompletion(driver, path); | |
| if (result != null) return result; | |
| try { | |
| return await driver.resolveForCompletion( | |
| path: path, | |
| offset: offset, | |
| performance: performance, | |
| ); | |
| } catch (exception, stackTrace) { | |
| instrumentationService.logException(exception, stackTrace); | |
| sessionLogger.logException(exception: exception, stackTrace: stackTrace); | |
| } | |
| return null; | |
| } | |
| Future<ResolvedForCompletionResultImpl?> resolveForCompletionWithLineColumn({ | |
| required String path, | |
| required int line, | |
| required int column, | |
| required OperationPerformanceImpl performance, | |
| }) async { | |
| if (!file_paths.isDart(resourceProvider.pathContext, path)) { | |
| return null; | |
| } | |
| var driver = getAnalysisDriver(path); | |
| if (driver == null) { | |
| return null; | |
| } | |
| var result = _tryGetCachedResolvedUnitAsResolvedForCompletion(driver, path); | |
| if (result != null) return result; | |
| try { | |
| return await driver.resolveForCompletionWithLineColumn( | |
| path: path, | |
| line: line, | |
| column: column, | |
| performance: performance, | |
| ); | |
| } catch (exception, stackTrace) { | |
| instrumentationService.logException(exception, stackTrace); | |
| sessionLogger.logException(exception: exception, stackTrace: stackTrace); | |
| } | |
| return null; | |
| } | |
| /// Sends an LSP notification to the client. | |
| /// | |
| /// The legacy server will wrap LSP notifications inside an | |
| /// 'lsp.notification' notification. | |
| void sendLspNotification(lsp.NotificationMessage notification); | |
| /// Sends an LSP request with the given [params] to the client and waits for a | |
| /// response. Completes with the raw [lsp.ResponseMessage] which could be an | |
| /// error response. | |
| Future<lsp.ResponseMessage> sendLspRequest(lsp.Method method, Object params); | |
| /// Sends an error notification to the user. | |
| void sendServerErrorNotification( | |
| String message, | |
| Object exception, | |
| StackTrace? stackTrace, { | |
| bool fatal = false, | |
| }); | |
| /// Shows the user a prompt with some actions to select from using | |
| /// 'window/showMessageRequest'. | |
| /// | |
| /// Callers should use [userPromptSender] instead of calling this method | |
| /// directly because it returns null if the client does not support user | |
| /// prompts. | |
| @visibleForOverriding | |
| Future<String?> showUserPrompt( | |
| MessageType type, | |
| String message, | |
| List<String> actionLabels, | |
| lsp.CancellationToken cancellationToken, | |
| ); | |
| @mustCallSuper | |
| Future<void> shutdown() async { | |
| await performanceLogger?.shutdown(); | |
| completeLspUninitialization(); | |
| await analysisDriverSchedulerEventsSubscription?.cancel(); | |
| analysisDriverSchedulerEventsSubscription = null; | |
| // For now we record plugins only on shutdown. We might want to record them | |
| // every time the set of plugins changes, in which case we'll need to listen | |
| // to the `PluginManager.pluginsChanged` stream. | |
| await analyticsManager.changedPlugins(pluginManager); | |
| // For now we record context-dependent information only on shutdown. We | |
| // might want to record it on start-up as well. | |
| analyticsManager.createdAnalysisContexts(contextManager.analysisContexts); | |
| pubPackageService.shutdown(); | |
| surveyManager?.shutdown(); | |
| await contextManager.dispose(); | |
| await _fileByteStore?.flush(); | |
| await analyticsManager.shutdown(); | |
| await shutdownPerfWitness(); | |
| await sessionLogger.shutdown(); | |
| } | |
| ResolvedForCompletionResultImpl? | |
| _tryGetCachedResolvedUnitAsResolvedForCompletion( | |
| AnalysisDriver driver, | |
| String path, | |
| ) { | |
| try { | |
| ResolvedUnitResult? result = driver.getCachedResolvedUnit(path); | |
| if (result != null) { | |
| result as ResolvedUnitResultImpl; | |
| return ResolvedForCompletionResultImpl( | |
| analysisSession: result.session, | |
| fileState: result.fileState, | |
| path: path, | |
| uri: result.uri, | |
| exists: result.exists, | |
| content: result.content, | |
| lineInfo: result.lineInfo, | |
| parsedUnit: result.unit, | |
| libraryFragment: result.libraryFragment, | |
| resolvedNodes: [result.unit], | |
| ); | |
| } | |
| } catch (exception, stackTrace) { | |
| instrumentationService.logException(exception, stackTrace); | |
| sessionLogger.logException(exception: exception, stackTrace: stackTrace); | |
| } | |
| return null; | |
| } | |
| } | |
| /// A snapshot of the sizes and counters of the byte stores used by the | |
| /// server, for display on the diagnostics pages. | |
| /// | |
| /// The file byte store fields are `null` when the server uses only an | |
| /// in-memory byte store. | |
| class AnalysisServerByteStoreStats { | |
| final int cacheHitCount; | |
| final int cacheMissCount; | |
| final int? cleanUpCount; | |
| final int currentSizeBytes; | |
| final int? deletedBytes; | |
| final int? deletedFileCount; | |
| final int entryCount; | |
| final int evictedBytes; | |
| final int evictedEntryCount; | |
| final int evictionCount; | |
| final int? failedReadCount; | |
| final int? failedWriteCount; | |
| final int? fileCacheSizeBytes; | |
| final String? fileStorePath; | |
| final int? fileStoreSizeBytes; | |
| final int? lastCleanUpTimeMilliseconds; | |
| final int? lastScannedFileCount; | |
| final int maxSizeBytes; | |
| final int? pendingWriteCount; | |
| final int putCount; | |
| final int? readCount; | |
| final int? readMissCount; | |
| final int storeHitCount; | |
| final int storeMissCount; | |
| final int? writeBytes; | |
| final int? writeCount; | |
| const new({ | |
| required this.cacheHitCount, | |
| required this.cacheMissCount, | |
| required this.currentSizeBytes, | |
| required this.entryCount, | |
| required this.evictedBytes, | |
| required this.evictedEntryCount, | |
| required this.evictionCount, | |
| required this.maxSizeBytes, | |
| required this.putCount, | |
| required this.storeHitCount, | |
| required this.storeMissCount, | |
| this.cleanUpCount, | |
| this.deletedBytes, | |
| this.deletedFileCount, | |
| this.failedReadCount, | |
| this.failedWriteCount, | |
| this.fileCacheSizeBytes, | |
| this.fileStorePath, | |
| this.fileStoreSizeBytes, | |
| this.lastCleanUpTimeMilliseconds, | |
| this.lastScannedFileCount, | |
| this.pendingWriteCount, | |
| this.readCount, | |
| this.readMissCount, | |
| this.writeBytes, | |
| this.writeCount, | |
| }); | |
| bool get usesFileByteStore => fileStorePath != null; | |
| } | |
| /// ContextManager callbacks that operate on the base server regardless | |
| /// of protocol. | |
| abstract class CommonServerContextManagerCallbacks | |
| extends ContextManagerCallbacks { | |
| /// The [ResourceProvider] by which paths are converted into [Resource]s. | |
| final OverlayResourceProvider resourceProvider; | |
| /// The set of files for which notifications were sent. | |
| final Set<String> filesToFlush = {}; | |
| new(this.resourceProvider); | |
| @override | |
| @mustCallSuper | |
| void afterContextsCreated() { | |
| analysisServer.afterContextsCreated(); | |
| } | |
| @override | |
| @mustCallSuper | |
| void afterContextsDestroyed() { | |
| flushResults(filesToFlush.toList()); | |
| filesToFlush.clear(); | |
| analysisServer.afterContextsDestroyed(); | |
| } | |
| @override | |
| void afterWatchEvent(WatchEvent event) {} | |
| @override | |
| @mustCallSuper | |
| void applyFileRemoved(String file) { | |
| // If the removed file doesn't have an overlay, we need to clear any | |
| // previous results. | |
| if (!resourceProvider.hasOverlay(file)) { | |
| // Clear the cached errors in the notification manager so we don't | |
| // re-send stale results if a plugin sends an update and we merge it with | |
| // previous results. | |
| analysisServer.notificationManager.errors.clearResultsForFile(file); | |
| if (filesToFlush.remove(file)) { | |
| flushResults([file]); | |
| } | |
| } | |
| } | |
| @override | |
| @mustCallSuper | |
| void broadcastWatchEvent(WatchEvent event) { | |
| analysisServer.notifyFlutterWidgetDescriptions(event.path); | |
| analysisServer.pluginManager.broadcastWatchEvent(event); | |
| } | |
| void flushResults(List<String> files); | |
| @override | |
| @mustCallSuper | |
| void handleFileResult(FileResult result) { | |
| if (analysisServer.suppressAnalysisResults) { | |
| return; | |
| } | |
| var path = result.path; | |
| filesToFlush.add(path); | |
| if (result is AnalysisResultWithDiagnostics) { | |
| if (analysisServer.isAnalyzed(path)) { | |
| var serverErrors = server.doAnalysisError_listFromEngine(result); | |
| recordAnalysisErrors(path, serverErrors); | |
| } | |
| } | |
| if (result is ResolvedUnitResult) { | |
| analysisServer.filesResolvedSinceLastIdle.add(path); | |
| handleResolvedUnitResult(result); | |
| } | |
| } | |
| void handleResolvedUnitResult(ResolvedUnitResult result); | |
| @override | |
| @mustCallSuper | |
| void listenAnalysisDriver(analysis.AnalysisDriver driver) { | |
| driver.exceptions.listen(analysisServer.logExceptionResult); | |
| driver.priorityFiles = analysisServer.priorityFiles.toList(); | |
| } | |
| @override | |
| void pubspecChanged(String path) { | |
| analysisServer.pubPackageService.fetchPackageVersionsViaPubOutdated( | |
| path, | |
| pubspecWasModified: true, | |
| ); | |
| } | |
| @override | |
| void pubspecRemoved(String path) { | |
| analysisServer.pubPackageService.flushPackageCaches(path); | |
| } | |
| @override | |
| @mustCallSuper | |
| void recordAnalysisErrors(String path, List<server.AnalysisError> errors) { | |
| filesToFlush.add(path); | |
| analysisServer.notificationManager.recordAnalysisErrors( | |
| NotificationManager.serverId, | |
| path, | |
| errors, | |
| ); | |
| } | |
| } | |
| /// A server-agnostic type for messages sent to the client. | |
| enum MessageType { | |
| error(lsp.MessageType.Error, legacy.MessageType.ERROR), | |
| warning(lsp.MessageType.Warning, legacy.MessageType.WARNING), | |
| info(lsp.MessageType.Info, legacy.MessageType.INFO), | |
| log(lsp.MessageType.Log, legacy.MessageType.LOG); | |
| final lsp.MessageType forLsp; | |
| final legacy.MessageType forLegacy; | |
| new(this.forLsp, this.forLegacy); | |
| } | |
| class ServerRecentPerformance { | |
| /// The maximum number of performance measurements to keep. | |
| static const int performanceListMaxLength = 250; | |
| /// The maximum number of slow performance measurements to keep. | |
| static const int slowRequestsListMaxLength = 1000; | |
| /// The duration we use to categorize requests as slow. | |
| static const Duration slowRequestsThreshold = Duration(milliseconds: 500); | |
| /// A list of code completion performance measurements for the latest | |
| /// completion operation up to [performanceListMaxLength] measurements. | |
| final RecentBuffer<CompletionPerformance> completion = | |
| RecentBuffer<CompletionPerformance>(performanceListMaxLength); | |
| /// A list of performance measurements for the latest requests to get fixes | |
| /// up to [performanceListMaxLength] measurements. | |
| final RecentBuffer<GetAssistsPerformance> getAssists = | |
| RecentBuffer<GetAssistsPerformance>(performanceListMaxLength); | |
| /// A list of performance measurements for the latest requests to get fixes | |
| /// up to [performanceListMaxLength] measurements. | |
| final RecentBuffer<GetFixesPerformance> getFixes = | |
| RecentBuffer<GetFixesPerformance>(performanceListMaxLength); | |
| /// A list of performance measurements for the latest requests to get refactorings | |
| /// up to [performanceListMaxLength] measurements. | |
| final RecentBuffer<GetRefactoringsPerformance> getRefactorings = | |
| RecentBuffer<GetRefactoringsPerformance>(performanceListMaxLength); | |
| /// A [RecentBuffer] for performance information about the most recent | |
| /// requests. | |
| final RecentBuffer<RequestPerformance> requests = RecentBuffer( | |
| performanceListMaxLength, | |
| ); | |
| /// A [RecentBuffer] for performance information about the most recent | |
| /// slow requests. | |
| final RecentBuffer<RequestPerformance> slowRequests = RecentBuffer( | |
| slowRequestsListMaxLength, | |
| ); | |
| } | |
| extension on OverlayResourceProvider { | |
| /// The path to the location of the byte store on disk, or `null` if there is | |
| /// no on-disk byte store. | |
| String? get byteStorePath { | |
| if (baseProvider is PhysicalResourceProvider) { | |
| var stateLocation = baseProvider.getStateLocation('.analysis-driver'); | |
| return stateLocation?.path; | |
| } | |
| return null; | |
| } | |
| } |