| // Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'dart:async'; | |
| import 'package:analysis_server/lsp_protocol/protocol.dart' hide MessageType; | |
| import 'package:analysis_server/src/analysis_server.dart'; | |
| import 'package:analysis_server/src/analytics/analytics_manager.dart'; | |
| import 'package:analysis_server/src/computer/computer_closing_labels.dart'; | |
| import 'package:analysis_server/src/computer/computer_outline.dart'; | |
| import 'package:analysis_server/src/flutter/flutter_outline_computer.dart'; | |
| import 'package:analysis_server/src/legacy_analysis_server.dart'; | |
| import 'package:analysis_server/src/lsp/channel/lsp_channel.dart'; | |
| import 'package:analysis_server/src/lsp/client_capabilities.dart'; | |
| import 'package:analysis_server/src/lsp/client_configuration.dart'; | |
| import 'package:analysis_server/src/lsp/constants.dart'; | |
| import 'package:analysis_server/src/lsp/handlers/handler_states.dart'; | |
| import 'package:analysis_server/src/lsp/handlers/handlers.dart'; | |
| import 'package:analysis_server/src/lsp/mapping.dart'; | |
| import 'package:analysis_server/src/lsp/notification_manager.dart'; | |
| import 'package:analysis_server/src/lsp/progress.dart'; | |
| import 'package:analysis_server/src/lsp/server_capabilities_computer.dart'; | |
| import 'package:analysis_server/src/protocol_server.dart' as protocol; | |
| import 'package:analysis_server/src/scheduler/message_scheduler.dart'; | |
| 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/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/session_logger/session_logger.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/error/error.dart' as engine; | |
| 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/context_locator.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/extensions/flutter.dart'; | |
| import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin; | |
| import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin; | |
| import 'package:analyzer_plugin/src/protocol/protocol_internal.dart' as plugin; | |
| import 'package:http/http.dart' as http; | |
| import 'package:meta/meta.dart'; | |
| /// Instances of the class [LspAnalysisServer] implement an LSP-based server | |
| /// that listens on a [LspServerCommunicationChannel] for LSP messages and | |
| /// processes them. | |
| class LspAnalysisServer extends AnalysisServer { | |
| /// The capabilities of the LSP client. Will be null prior to initialization. | |
| LspClientCapabilities? _clientCapabilities; | |
| /// Information about the connected client. Will be null prior to | |
| /// initialization or if the client did not provide it. | |
| ClientInfo? _clientInfo; | |
| /// Initialization options provided by the LSP client. Allows opting in/out of | |
| /// specific server functionality. Will be null prior to initialization. | |
| LspInitializationOptions? _initializationOptions; | |
| /// Configuration for the workspace from the client. This is similar to | |
| /// initializationOptions but can be updated dynamically rather than set | |
| /// only when the server starts. | |
| @override | |
| final LspClientConfiguration lspClientConfiguration; | |
| /// The channel from which messages are received and to which responses should | |
| /// be sent. | |
| final LspServerCommunicationChannel channel; | |
| /// The message handler for the server based on the current state | |
| /// (uninitialized, initializing, initialized, etc.). | |
| late ServerStateMessageHandler messageHandler; | |
| /// The ID that will be used for the next outbound server-to-client request. | |
| int nextRequestId = 1; | |
| /// Completers for the responses to outbound server-to-client requests. | |
| final Map<int, Completer<ResponseMessage>> completers = {}; | |
| /// Capabilities of the server. Will be null prior to initialization as | |
| /// the server capabilities depend on the client capabilities. | |
| ServerCapabilities? capabilities; | |
| late ServerCapabilitiesComputer capabilitiesComputer; | |
| /// Whether or not the server is controlling the shutdown and will exit | |
| /// automatically. | |
| bool willExit = false; | |
| StreamSubscription<void>? _pluginChangeSubscription; | |
| /// The current workspace folders provided by the client, used as analysis | |
| /// roots. | |
| /// | |
| /// If the user has enabled [onlyAnalyzeProjectsWithOpenFiles] this will never | |
| /// be populated, but instead the roots will be computed when they are | |
| /// refreshed. | |
| final _workspaceFolders = <String>{}; | |
| /// A progress reporter for analysis status. | |
| ProgressReporter? analyzingProgressReporter; | |
| /// The number of times contexts have been created/recreated. | |
| @visibleForTesting | |
| int contextBuilds = 0; | |
| /// An optional manager to handle file systems which may not always be | |
| /// available. | |
| final DetachableFileSystemManager? detachableFileSystemManager; | |
| /// A flag indicating whether analysis was being performed the last time | |
| /// `sendStatusNotification` was invoked. | |
| bool wasAnalyzing = false; | |
| /// Tracks files that have non-empty diagnostics on the client. | |
| /// | |
| /// This is an optimization to avoid sending empty diagnostics when they are | |
| /// unnecessary (at startup, when a file is re-analyzed because a file it | |
| /// imports was modified, etc). | |
| final Set<String> _filesWithClientDiagnostics = {}; | |
| /// A completer that tracks in-progress workspace folders update. | |
| /// | |
| /// Starts completed and will be replaced each time workspace folders are | |
| /// being updated (which can trigger async work such as fetching client | |
| /// configuration or dynamic registrations). | |
| Completer<void> workspaceFolderUpdateCompleter = Completer()..complete(); | |
| /// Initialize a newly created server to send and receive messages to the | |
| /// given [channel]. | |
| new( | |
| this.channel, | |
| ResourceProvider baseResourceProvider, | |
| AnalysisServerOptions options, | |
| DartSdkManager sdkManager, | |
| AnalyticsManager analyticsManager, | |
| CrashReportingAttachmentsBuilder crashReportingAttachmentsBuilder, | |
| InstrumentationService instrumentationService, | |
| SessionLogger sessionLogger, { | |
| http.Client? httpClient, | |
| ProcessRunner? processRunner, | |
| DiagnosticServer? diagnosticServer, | |
| this.detachableFileSystemManager, | |
| super.enableBlazeWatcher, | |
| super.dartFixPromptManager, | |
| super.providedByteStore, | |
| super.pluginManager, | |
| super.messageSchedulerListener, | |
| super.performanceLogger, | |
| super.environment, | |
| }) : lspClientConfiguration = LspClientConfiguration( | |
| baseResourceProvider.pathContext, | |
| ), | |
| super( | |
| options, | |
| sdkManager, | |
| diagnosticServer, | |
| analyticsManager, | |
| crashReportingAttachmentsBuilder, | |
| baseResourceProvider, | |
| instrumentationService, | |
| sessionLogger, | |
| httpClient, | |
| processRunner, | |
| LspNotificationManager(baseResourceProvider.pathContext), | |
| usePlugins: options.usePlugins, | |
| ) { | |
| notificationManager.server = this; | |
| messageHandler = UninitializedStateMessageHandler(this); | |
| capabilitiesComputer = ServerCapabilitiesComputer(this); | |
| var contextManagerCallbacks = LspServerContextManagerCallbacks( | |
| this, | |
| resourceProvider, | |
| ); | |
| contextManager.callbacks = contextManagerCallbacks; | |
| analysisDriverSchedulerEventsSubscription = analysisDriverScheduler.events | |
| .listen(handleAnalysisEvent); | |
| analysisDriverScheduler.start(); | |
| channel.listen(scheduleMessage, onDone: done, onError: socketError); | |
| _pluginChangeSubscription = pluginManager.pluginsChanged.listen( | |
| (_) => _onPluginsChanged(), | |
| ); | |
| notificationManager.pluginAnalysisStatusChanges.listen((isAnalyzing) { | |
| 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(); | |
| } | |
| // TODO(srawlins): Perform "on idle" tasks (Legacy server has | |
| // `_performOnIdleActions`). | |
| }); | |
| } | |
| /// The hosted location of the client application. | |
| /// | |
| /// This information is not part of the LSP spec so is only provided for | |
| /// clients extensions that add it to initialization options explicitly | |
| /// (such as Dart-Code for VS Code where it comes from 'env.appHost'). | |
| /// | |
| /// This value is usually 'desktop' for desktop installs and for web will be | |
| /// the name of the web embedder if provided (such as 'github.dev' or | |
| /// 'codespaces'), else 'web'. | |
| String? get clientAppHost => _initializationOptions?.appHost; | |
| /// Information about the connected editor client. Will be `null` prior to | |
| /// initialization. | |
| ClientInfo? get clientInfo => _clientInfo; | |
| /// The name of the remote when the client is running using a remote workspace. | |
| /// | |
| /// This information is not part of the LSP spec so is only provided for | |
| /// clients extensions that add it to initialization options explicitly | |
| /// (such as Dart-Code for VS Code where it comes from 'env.remoteName'). | |
| /// | |
| /// This value is `null` for local workspaces and will contain a string such | |
| /// as 'ssh-remote' or 'wsl' for remote workspaces. | |
| String? get clientRemoteName => _initializationOptions?.remoteName; | |
| @override | |
| List<String> get configurationWorkspaceFolders => _workspaceFolders.toList(); | |
| /// The capabilities of the LSP client. Will be null prior to initialization. | |
| @override | |
| LspClientCapabilities? get editorClientCapabilities => _clientCapabilities; | |
| Future<void> get exited => channel.closed; | |
| /// Initialization options provided by the LSP client. Allows opting in/out of | |
| /// specific server functionality. Will be null prior to initialization. | |
| @override | |
| LspInitializationOptions? get initializationOptions => _initializationOptions; | |
| /// Whether the server has transitioned into the shutting down state. | |
| bool get isShuttingDown => messageHandler is ShuttingDownStateMessageHandler; | |
| @override | |
| LspNotificationManager get notificationManager => | |
| super.notificationManager as LspNotificationManager; | |
| /// Whether the user has enabled the `dart.onlyAnalyzeProjectsWithOpenFiles` | |
| /// setting. | |
| /// | |
| /// This setting tells us to ignore the workspace folders from the client | |
| /// and instead compute analysis roots from the open files as files are | |
| /// opened/closed. | |
| /// | |
| /// [_workspaceFolders] will always be empty in this mode. | |
| bool get onlyAnalyzeProjectsWithOpenFiles => | |
| _initializationOptions?.onlyAnalyzeProjectsWithOpenFiles ?? false; | |
| @override | |
| OpenUriNotificationSender? get openUriNotificationSender { | |
| if (!(initializationOptions?.allowOpenUri ?? false)) { | |
| return null; | |
| } | |
| return (Uri uri) async { | |
| var params = OpenUriParams(uri: uri); | |
| var message = NotificationMessage( | |
| method: CustomMethods.openUri, | |
| params: params, | |
| jsonrpc: jsonRpcVersion, | |
| ); | |
| sendLspNotification(message); | |
| }; | |
| } | |
| /// Whether or not the client has advertised support for | |
| /// 'window/showMessageRequest'. | |
| /// | |
| /// Callers should use [userPromptSender] instead of checking this directly. | |
| @override | |
| @protected | |
| bool get supportsShowMessageRequest => | |
| editorClientCapabilities?.supportsShowMessageRequest ?? false; | |
| /// A [Future] that completes when any in-progress workspace folder update | |
| /// completes. | |
| /// | |
| /// If no workspace folder update is in progress, will return an already complete | |
| /// [Future]. | |
| Future<void> get workspaceFolderUpdate => | |
| workspaceFolderUpdateCompleter.future; | |
| Future<void> addPriorityFile(String filePath) async { | |
| // When pubspecs are opened, trigger pre-loading of pub package names and | |
| // versions. | |
| if (file_paths.isPubspecYaml(resourceProvider.pathContext, filePath)) { | |
| pubPackageService.beginCachePreloads([filePath]); | |
| } | |
| var didAdd = priorityFiles.add(filePath); | |
| assert(didAdd); | |
| if (didAdd) { | |
| _updateDriversAndPluginsPriorityFiles(); | |
| // If there are no explicit analysis roots, they are inferred from open | |
| // files and so must be recomputed. | |
| if (_workspaceFolders.isEmpty) { | |
| await refreshAnalysisRoots(); | |
| } | |
| } | |
| } | |
| /// The socket from which messages are being read has been closed. | |
| void done() {} | |
| /// Fetches the configuration from the client (if supported) and then sends | |
| /// register/unregister requests for any supported/enabled dynamic | |
| /// registrations. | |
| @override | |
| Future<void> fetchClientConfiguration() async { | |
| await super.fetchClientConfiguration(); | |
| // Client config can affect capabilities, so this should only be done after | |
| // we have the initial/updated config. | |
| // Don't await this because it involves sending requests to the client (for | |
| // config) that should not stop/delay initialization. | |
| unawaited(capabilitiesComputer.performDynamicRegistration()); | |
| } | |
| @override | |
| FutureOr<void> handleAnalysisStatusChange( | |
| analysis.AnalysisStatus status, | |
| ) async { | |
| await super.handleAnalysisStatusChange(status); | |
| await sendStatusNotification(status); | |
| } | |
| void handleClientConnection( | |
| ClientCapabilities capabilities, | |
| ClientInfo? clientInfo, | |
| Object? rawInitializationOptions, | |
| ) { | |
| _clientCapabilities = LspClientCapabilities(capabilities); | |
| _clientInfo = clientInfo; | |
| var initializationOptions = _initializationOptions = | |
| LspInitializationOptions(rawInitializationOptions); | |
| // Set whether to allow interleaved requests. | |
| if (initializationOptions.allowOverlappingHandlers | |
| case var allowOverlappingHandlers?) { | |
| MessageScheduler.allowOverlappingHandlers = allowOverlappingHandlers; | |
| instrumentationService.logInfo( | |
| 'MessageScheduler.allowOverlappingHandlers set to ' | |
| '$allowOverlappingHandlers by LSP client initializationOptions', | |
| ); | |
| sessionLogger.logInfo( | |
| 'MessageScheduler.allowOverlappingHandlers set to ' | |
| '$allowOverlappingHandlers by LSP client initializationOptions', | |
| ); | |
| } | |
| performanceAfterStartup = ServerPerformance(); | |
| performance = performanceAfterStartup!; | |
| checkAnalytics(); | |
| enableSurveys(); | |
| // Notify the client of any issues parsing experimental capabilities. | |
| var errors = _clientCapabilities?.experimentalCapabilitiesErrors; | |
| if (errors != null && errors.isNotEmpty) { | |
| var message = | |
| 'There were errors parsing your experimental ' | |
| 'client capabilities:\n${errors.join(', ')}'; | |
| var params = ShowMessageParams( | |
| type: MessageType.warning.forLsp, | |
| message: message, | |
| ); | |
| channel.sendNotification( | |
| NotificationMessage( | |
| method: Method.window_showMessage, | |
| params: params, | |
| jsonrpc: jsonRpcVersion, | |
| ), | |
| ); | |
| } | |
| } | |
| /// Handles a response from the client by invoking the completer that the | |
| /// outbound request created. | |
| void handleClientResponse(ResponseMessage message) { | |
| // The ID from the client is an Either2<num, String>?, though it's not valid | |
| // for it to be a null or a string because it should match a request we sent | |
| // to the client (and we always use numeric IDs for outgoing requests). | |
| var id = message.id; | |
| if (id == null) { | |
| showErrorMessageToUser('Unexpected response with no ID!'); | |
| return; | |
| } | |
| id.map( | |
| (id) { | |
| // It's possible that even if we got a numeric ID that it's not valid. | |
| // If it's not in our completers list (which is a list of the | |
| // outstanding requests we've sent) then show an error. | |
| var completer = completers[id]; | |
| if (completer == null) { | |
| showErrorMessageToUser('Response with ID $id was unexpected'); | |
| } else { | |
| completers.remove(id); | |
| completer.complete(message); | |
| } | |
| }, | |
| (stringID) { | |
| showErrorMessageToUser('Unexpected String ID for response $stringID'); | |
| }, | |
| ); | |
| } | |
| /// Handle a [message] that was read from the communication channel. The | |
| /// [completer] is used to indicate when the [RequestMessage] handling is | |
| /// done. [ResponseMessage] and [NotificationMessage]'s are not waited on | |
| /// till finish. | |
| void handleMessage( | |
| Message message, | |
| Completer<void>? completer, { | |
| CancelableToken? cancellationToken, | |
| }) { | |
| var startTime = DateTime.now(); | |
| performance.logRequestTiming(message.clientRequestTime); | |
| runZonedGuarded( | |
| () async { | |
| try { | |
| if (message is ResponseMessage) { | |
| handleClientResponse(message); | |
| } else if (message is IncomingMessage) { | |
| // Record performance information for the request. | |
| var rootPerformance = OperationPerformanceImpl('<root>'); | |
| RequestPerformance? requestPerformance; | |
| await rootPerformance.runAsync('request[${message.method}]', ( | |
| performance, | |
| ) async { | |
| requestPerformance = RequestPerformance( | |
| operation: message.method.toString(), | |
| performance: performance, | |
| requestLatency: message.timeSinceRequest, | |
| startTime: startTime, | |
| ); | |
| recentPerformance.requests.add(requestPerformance!); | |
| var messageInfo = MessageInfo( | |
| performance: performance, | |
| clientCapabilities: editorClientCapabilities, | |
| timeSinceRequest: message.timeSinceRequest, | |
| completer: completer, | |
| isTrustedCaller: true, | |
| ); | |
| if (message is RequestMessage) { | |
| analyticsManager.startedRequestMessage( | |
| request: message, | |
| startTime: startTime, | |
| ); | |
| await _handleRequestMessage( | |
| message, | |
| messageInfo, | |
| cancellationToken: cancellationToken, | |
| ); | |
| } else if (message is NotificationMessage) { | |
| await _handleNotificationMessage(message, messageInfo); | |
| analyticsManager.handledNotificationMessage( | |
| notification: message, | |
| startTime: startTime, | |
| endTime: DateTime.now(), | |
| ); | |
| } else { | |
| showErrorMessageToUser('Unknown incoming message type'); | |
| } | |
| }); | |
| if (requestPerformance != null && | |
| requestPerformance!.performance.elapsed > | |
| ServerRecentPerformance.slowRequestsThreshold) { | |
| recentPerformance.slowRequests.add(requestPerformance!); | |
| } | |
| } else { | |
| showErrorMessageToUser('Unknown message type'); | |
| } | |
| completer?.setComplete(); | |
| } on InconsistentAnalysisException { | |
| sendErrorResponse( | |
| message, | |
| ResponseError( | |
| code: ErrorCodes.ContentModified, | |
| message: 'Document was modified before operation completed', | |
| ), | |
| ); | |
| completer?.setComplete(); | |
| } catch (error, stackTrace) { | |
| var errorMessage = message is ResponseMessage | |
| ? 'An error occurred while handling the response to request ${message.id}' | |
| : message is RequestMessage | |
| ? 'An error occurred while handling ${message.method} request' | |
| : message is NotificationMessage | |
| ? 'An error occurred while handling ${message.method} notification' | |
| : 'Unknown message type'; | |
| sendErrorResponse( | |
| message, | |
| ResponseError( | |
| code: ServerErrorCodes.unhandledError, | |
| message: errorMessage, | |
| ), | |
| ); | |
| logException(errorMessage, error, stackTrace); | |
| completer?.setComplete(); | |
| } | |
| }, | |
| unhandledZoneError, | |
| zoneValues: { | |
| // A container for capturing additional timings (such as analysis) stored | |
| // in zoneValues so it can be accessed anywhere in the processing of this | |
| // request. | |
| RequestPerformanceAdditionalTimings.zoneValueKey: | |
| RequestPerformanceAdditionalTimings(), | |
| }, | |
| ); | |
| } | |
| /// Logs the error on the client using window/logMessage. | |
| void logErrorToClient(String message) { | |
| channel.sendNotification( | |
| NotificationMessage( | |
| method: Method.window_logMessage, | |
| params: LogMessageParams( | |
| type: MessageType.error.forLsp, | |
| message: message, | |
| ), | |
| jsonrpc: jsonRpcVersion, | |
| ), | |
| ); | |
| } | |
| /// Logs an exception by sending it to the client (window/logMessage) and | |
| /// recording it in a buffer on the server for diagnostics. | |
| void logException(String message, Object exception, StackTrace? stackTrace) { | |
| var fullMessage = message; | |
| if (exception is CaughtException) { | |
| stackTrace ??= exception.stackTrace; | |
| fullMessage = '$fullMessage: ${exception.exception}'; | |
| } else { | |
| fullMessage = '$fullMessage: $exception'; | |
| } | |
| var fullError = stackTrace == null | |
| ? fullMessage | |
| : '$fullMessage\n$stackTrace'; | |
| stackTrace ??= StackTrace.current; | |
| // Log the full message since showMessage above may be truncated or | |
| // formatted badly (eg. VS Code takes the newlines out). | |
| logErrorToClient(fullError); | |
| // remember the last few exceptions | |
| exceptions.add(ServerException(message, exception, stackTrace, false)); | |
| var fatalException = FatalException(message, exception, stackTrace); | |
| var attachments = crashReportingAttachmentsBuilder.forException(exception); | |
| instrumentationService.logException(fatalException, null, attachments); | |
| sessionLogger.logException( | |
| exception: fatalException, | |
| attachments: attachments, | |
| ); | |
| } | |
| void onOverlayCreated(String path, String content) { | |
| resourceProvider.setOverlay( | |
| path, | |
| content: content, | |
| modificationStamp: overlayModificationStamp++, | |
| ); | |
| // If the overlay is exactly the same as the previous content we can skip | |
| // notifying drivers which avoids re-analyzing the same content. | |
| // We use getAnalysisDriver() here because it can get content from | |
| // dependencies (so the optimization works for them) but below we use | |
| // `contextManager.driverFor` so we only add to the specific driver. | |
| var driver = getAnalysisDriver(path); | |
| var state = driver?.fsState.getExistingFromPath(path); | |
| // Content is updated if we have no state, we have state that does not exist | |
| // or the content differs. For a file with state that does not exist, the | |
| // content will be an empty string, so we must check exists to cover files | |
| // that are newly created with empty contents. | |
| var contentIsUpdated = | |
| state == null || !state.exists || state.content != content; | |
| if (contentIsUpdated) { | |
| _afterOverlayChanged(path, plugin.AddContentOverlay(content)); | |
| // 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(path)?.addFile(path); | |
| } else { | |
| // If we skip the work above, we still need to ensure plugins are notified | |
| // of the new overlay (which usually happens in `_afterOverlayChanged`). | |
| _notifyPluginsOverlayChanged(path, plugin.AddContentOverlay(content)); | |
| // We also need to ensure notifications like Outline can still sent in | |
| // this case (which are usually triggered by the re-analysis), so force | |
| // sending the resolved unit to the result stream even if we didn't need | |
| // to re-analyze it. | |
| unawaited(getResolvedUnit(path, sendCachedToStream: true)); | |
| } | |
| } | |
| void onOverlayDestroyed(String path) { | |
| resourceProvider.removeOverlay(path); | |
| _afterOverlayChanged(path, plugin.RemoveContentOverlay()); | |
| } | |
| /// Updates an overlay on [path] by applying the [edits] to the current | |
| /// overlay with the result [newContent]. | |
| void onOverlayUpdated( | |
| String path, | |
| List<plugin.SourceEdit> edits, { | |
| required String newContent, | |
| }) { | |
| assert(resourceProvider.hasOverlay(path)); | |
| resourceProvider.setOverlay( | |
| path, | |
| content: newContent, | |
| modificationStamp: overlayModificationStamp++, | |
| ); | |
| _afterOverlayChanged( | |
| path, | |
| plugin.ChangeContentOverlay(edits), | |
| precomputedNewContentForChange: newContent, | |
| ); | |
| } | |
| void publishDiagnostics(String path, List<Diagnostic> errors) { | |
| if (errors.isEmpty && !_filesWithClientDiagnostics.contains(path)) { | |
| // Don't sent empty set if client is already empty. | |
| return; | |
| } | |
| if (errors.isEmpty) { | |
| _filesWithClientDiagnostics.remove(path); | |
| } else { | |
| _filesWithClientDiagnostics.add(path); | |
| } | |
| var params = PublishDiagnosticsParams( | |
| uri: uriConverter.toClientUri(path), | |
| diagnostics: errors, | |
| ); | |
| var message = NotificationMessage( | |
| method: Method.textDocument_publishDiagnostics, | |
| params: params, | |
| jsonrpc: jsonRpcVersion, | |
| ); | |
| sendLspNotification(message); | |
| } | |
| void publishFlutterOutline(String path, FlutterOutline outline) { | |
| var params = PublishFlutterOutlineParams( | |
| uri: uriConverter.toClientUri(path), | |
| outline: outline, | |
| ); | |
| var message = NotificationMessage( | |
| method: CustomMethods.publishFlutterOutline, | |
| params: params, | |
| jsonrpc: jsonRpcVersion, | |
| ); | |
| sendLspNotification(message); | |
| } | |
| void publishOutline(String path, Outline outline) { | |
| var params = PublishOutlineParams( | |
| uri: uriConverter.toClientUri(path), | |
| outline: outline, | |
| ); | |
| var message = NotificationMessage( | |
| method: CustomMethods.publishOutline, | |
| params: params, | |
| jsonrpc: jsonRpcVersion, | |
| ); | |
| sendLspNotification(message); | |
| } | |
| @override | |
| @protected | |
| Future<void> refreshAnalysisRoots() async { | |
| // When there are open folders, they are always the roots. If there are no | |
| // open workspace folders, then we use the open (priority) files to compute | |
| // roots. | |
| var includedPaths = _workspaceFolders.isNotEmpty | |
| ? _workspaceFolders.toList() | |
| : _getRootsForOpenFiles().toList(); | |
| var excludedPaths = lspClientConfiguration.global.analysisExcludedFolders | |
| .expand( | |
| (excludePath) => resourceProvider.pathContext.isAbsolute(excludePath) | |
| ? [excludePath] | |
| // Apply the relative path to each open workspace folder. | |
| // TODO(dantup): Consider supporting per-workspace config by | |
| // calling workspace/configuration whenever workspace folders change | |
| // and caching the config for each one. | |
| : _workspaceFolders.map( | |
| (root) => | |
| resourceProvider.pathContext.join(root, excludePath), | |
| ), | |
| ) | |
| .map(pathContext.normalize) | |
| .toSet() | |
| .toList(); | |
| notificationManager.setAnalysisRoots(includedPaths, excludedPaths); | |
| if (detachableFileSystemManager != null) { | |
| detachableFileSystemManager?.setAnalysisRoots( | |
| null, | |
| includedPaths, | |
| excludedPaths, | |
| ); | |
| } else { | |
| var completer = analysisContextRebuildCompleter = Completer(); | |
| try { | |
| await contextManager.setRoots(includedPaths, excludedPaths); | |
| } finally { | |
| completer.complete(); | |
| } | |
| } | |
| pluginManager.setAnalysisSetAnalysisRootsParams( | |
| plugin.AnalysisSetAnalysisRootsParams(includedPaths, excludedPaths), | |
| ); | |
| // If there are no analysis roots, no drivers will be created and the plugin | |
| // watcher will not complete the plugin managers initialization so we need | |
| // to do it. | |
| if (includedPaths.isEmpty && | |
| !pluginManager.initializedCompleter.isCompleted) { | |
| pluginManager.initializedCompleter.complete(); | |
| } | |
| } | |
| Future<void> removePriorityFile(String path) async { | |
| var didRemove = priorityFiles.remove(path); | |
| assert(didRemove); | |
| if (didRemove) { | |
| _updateDriversAndPluginsPriorityFiles(); | |
| // If there are no explicit analysis roots, they are inferred from open | |
| // files and so must be recomputed. | |
| if (_workspaceFolders.isEmpty) { | |
| await refreshAnalysisRoots(); | |
| } | |
| } | |
| } | |
| void scheduleMessage(Message message) { | |
| CancelableToken? cancellationToken; | |
| // Create a cancellation token that will allow us to cancel | |
| // this request if requested. | |
| if (message is RequestMessage) { | |
| cancellationToken = messageHandler.cancelHandler.createToken(message); | |
| } | |
| messageScheduler.add( | |
| LspMessage(message: message, cancellationToken: cancellationToken), | |
| ); | |
| } | |
| void sendErrorResponse(Message message, ResponseError error) { | |
| if (message is RequestMessage) { | |
| sendResponse( | |
| ResponseMessage(id: message.id, error: error, jsonrpc: jsonRpcVersion), | |
| ); | |
| } else if (message is ResponseMessage) { | |
| // For bad response messages where we can't respond with an error, send it | |
| // as show instead of log. | |
| showErrorMessageToUser(error.message); | |
| } else { | |
| // For notifications where we couldn't respond with an error, send it as | |
| // show instead of log. | |
| showErrorMessageToUser(error.message); | |
| } | |
| // Handle fatal errors where the client/server state is out of sync and we | |
| // should not continue. | |
| if (error.code == ServerErrorCodes.clientServerInconsistentState) { | |
| // Do not process any further messages. | |
| messageHandler = FailureStateMessageHandler(this); | |
| var message = 'An unrecoverable error occurred.'; | |
| logErrorToClient( | |
| '$message\n\n${error.message}\n\n${error.code}\n\n${error.data}', | |
| ); | |
| unawaited(shutdown()); | |
| } | |
| } | |
| /// Send the given [notification] to the client. | |
| @override | |
| void sendLspNotification(NotificationMessage notification) { | |
| channel.sendNotification(notification); | |
| } | |
| @override | |
| Future<ResponseMessage> sendLspRequest(Method method, Object params) { | |
| var requestId = nextRequestId++; | |
| var completer = Completer<ResponseMessage>(); | |
| completers[requestId] = completer; | |
| channel.sendRequest( | |
| RequestMessage( | |
| id: Either2<int, String>.t1(requestId), | |
| method: method, | |
| params: params, | |
| jsonrpc: jsonRpcVersion, | |
| ), | |
| ); | |
| return completer.future; | |
| } | |
| /// Send the given [response] to the client. | |
| void sendResponse(ResponseMessage response) { | |
| channel.sendResponse(response); | |
| analyticsManager.sentResponseMessage(response: response); | |
| } | |
| @override | |
| void sendServerErrorNotification( | |
| String message, | |
| Object exception, | |
| StackTrace? stackTrace, { | |
| bool fatal = false, | |
| }) { | |
| message = '$message: $exception'; | |
| // Show message (without stack) to the user. | |
| showErrorMessageToUser(message); | |
| } | |
| /// Send status notification to the client. The state of analysis is given by | |
| /// the [status] information. | |
| Future<void> sendStatusNotification(analysis.AnalysisStatus status) async { | |
| // Send old custom notifications to clients that do not support $/progress. | |
| // TODO(dantup): Remove this custom notification (and related classes) when | |
| // it's unlikely to be in use by any clients. | |
| var isAnalyzing = status.isWorking; | |
| if (wasAnalyzing && !isAnalyzing) { | |
| wasAnalyzing = isAnalyzing; | |
| // Only send analysis analytics after analysis is complete. | |
| reportAnalysisAnalytics(); | |
| } | |
| if (isAnalyzing && !wasAnalyzing) { | |
| wasAnalyzing = true; | |
| } | |
| if (editorClientCapabilities?.workDoneProgress != true) { | |
| channel.sendNotification( | |
| NotificationMessage( | |
| method: CustomMethods.analyzerStatus, | |
| params: AnalyzerStatusParams(isAnalyzing: isAnalyzing), | |
| jsonrpc: jsonRpcVersion, | |
| ), | |
| ); | |
| return; | |
| } | |
| if (isAnalyzing) { | |
| analyzingProgressReporter ??= | |
| ProgressReporter.serverCreated(this, analyzingProgressToken) | |
| // ignore: unawaited_futures | |
| ..begin('Analyzing…'); | |
| } else { | |
| if (analyzingProgressReporter != null) { | |
| // Do not null this out until after end completes, otherwise we may try | |
| // to create a new token before it's really completed. | |
| await analyzingProgressReporter?.end(); | |
| analyzingProgressReporter = null; | |
| } | |
| } | |
| } | |
| /// Returns `true` if closing labels should be sent for [file] with the given | |
| /// absolute path. | |
| bool shouldSendClosingLabelsFor(String file) { | |
| // Closing labels should only be sent for open (priority) files in the | |
| // workspace. | |
| var enabled = | |
| // Legacy way for Dart-Code sending them | |
| (initializationOptions?.closingLabels ?? false) || | |
| // New way (for Dart-Code + LSP-over-Legacy). | |
| (_clientCapabilities?.closingLabels ?? false); | |
| return enabled && priorityFiles.contains(file) && isAnalyzed(file); | |
| } | |
| /// Returns `true` if Flutter outlines should be sent for [file] with the | |
| /// given absolute path. | |
| bool shouldSendFlutterOutlineFor(String file) { | |
| // Outlines should only be sent for open (priority) files in the workspace. | |
| return (initializationOptions?.flutterOutline ?? false) && | |
| priorityFiles.contains(file) && | |
| _isInFlutterProject(file); | |
| } | |
| /// Returns `true` if outlines should be sent for [file] with the given | |
| /// absolute path. | |
| bool shouldSendOutlineFor(String file) { | |
| // Outlines should only be sent for open (priority) files in the workspace. | |
| return (initializationOptions?.outline ?? false) && | |
| priorityFiles.contains(file); | |
| } | |
| void showErrorMessageToUser(String message) { | |
| showMessageToUser(MessageType.error, message); | |
| } | |
| void showMessageToUser(MessageType type, String message) { | |
| channel.sendNotification( | |
| NotificationMessage( | |
| method: Method.window_showMessage, | |
| params: ShowMessageParams(type: type.forLsp, message: message), | |
| jsonrpc: jsonRpcVersion, | |
| ), | |
| ); | |
| } | |
| /// 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. | |
| @override | |
| @visibleForOverriding | |
| Future<String?> showUserPrompt( | |
| MessageType type, | |
| String message, | |
| List<String> actions, | |
| CancellationToken cancellationToken, | |
| ) async { | |
| assert(supportsShowMessageRequest); | |
| var response = await showUserPromptItems( | |
| type, | |
| message, | |
| actions.map((title) => MessageActionItem(title: title)).toList(), | |
| cancellationToken, | |
| ); | |
| return response?.title; | |
| } | |
| /// Shows the user a prompt with some actions to select from using | |
| /// 'window/showMessageRequest'. | |
| /// | |
| /// Callers should verify the client supports 'window/showMessageRequest' with | |
| /// [supportsShowMessageRequest] before calling this, and handle cases where | |
| /// it is not appropriately. | |
| /// | |
| /// For simple cases, [showUserPrompt] provides a slightly simpler API using | |
| /// [String]s instead of [MessageActionItem]s. | |
| Future<MessageActionItem?> showUserPromptItems( | |
| MessageType type, | |
| String message, | |
| List<MessageActionItem> actions, | |
| CancellationToken cancellationToken, | |
| ) async { | |
| assert(supportsShowMessageRequest); | |
| var responseFuture = sendLspRequest( | |
| Method.window_showMessageRequest, | |
| ShowMessageRequestParams( | |
| type: type.forLsp, | |
| message: message, | |
| actions: actions, | |
| ), | |
| ); | |
| // 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; | |
| var result = response.result; | |
| return result != null | |
| ? MessageActionItem.fromJson(response.result as Map<String, Object?>) | |
| : null; | |
| } | |
| } | |
| @override | |
| Future<void> shutdown() async { | |
| await super.shutdown(); | |
| detachableFileSystemManager?.dispose(); | |
| // Defer closing the channel so that the shutdown response can be sent and | |
| // logged. | |
| unawaited( | |
| Future(() { | |
| channel.close(); | |
| }), | |
| ); | |
| unawaited(_pluginChangeSubscription?.cancel()); | |
| _pluginChangeSubscription = null; | |
| } | |
| /// There was an error related to the socket from which messages are being | |
| /// read. | |
| void socketError(Object error, StackTrace? stackTrace) { | |
| // Don't send to instrumentation service; not an internal error. Probably | |
| // caused by server shutdown or similar. | |
| sendServerErrorNotification('Socket error', error, stackTrace); | |
| } | |
| /// There was an unhandled async error in the zone used to execute the | |
| /// message handler. | |
| void unhandledZoneError(Object error, StackTrace? stackTrace) { | |
| // Record to the instrumentation log so the stack trace is accessible. This | |
| // is an unhandled exception that was not awaited and is almost certainly a | |
| // bug. | |
| instrumentationService.logException(error, stackTrace); | |
| sessionLogger.logException(exception: error, stackTrace: stackTrace); | |
| sendServerErrorNotification('Unhandled handler error', error, stackTrace); | |
| } | |
| /// Updates the active set of workspace folders. | |
| /// | |
| /// This is provided by the client at startup and may be updated by | |
| /// the `didChangeWorkspcaeFolders` request, however if the user has | |
| /// enabled [onlyAnalyzeProjectsWithOpenFiles] then this the set of workspace | |
| /// folders will always be empty (and computed when refreshing analysis | |
| /// roots). | |
| Future<void> updateWorkspaceFolders( | |
| List<String> addedPaths, | |
| List<String> removedPaths, | |
| ) async { | |
| // TODO(dantup): This is currently case-sensitive! | |
| // Normalize all potential workspace folder paths as these may contain | |
| // trailing slashes (the LSP spec does not specify whether folders | |
| // should/should not have them) and the analysis roots must be normalized. | |
| var pathContext = resourceProvider.pathContext; | |
| var addedNormalized = addedPaths.map(pathContext.normalize).toList(); | |
| var removedNormalized = removedPaths.map(pathContext.normalize).toList(); | |
| _workspaceFolders | |
| ..addAll(addedNormalized) | |
| ..removeAll(removedNormalized); | |
| // Capture if there is an existing update in progress, we so can wait for | |
| // it after replacing the completer. This is required because of the async | |
| // work below (`fetchClientConfiguration`) that could otherwise allow | |
| // another request to change the analysis roots before we've finished | |
| // setting them up correctly. | |
| var existingUpdateFuture = !workspaceFolderUpdateCompleter.isCompleted | |
| ? workspaceFolderUpdateCompleter.future | |
| : null; | |
| var completer = workspaceFolderUpdateCompleter = Completer(); | |
| if (existingUpdateFuture != null) { | |
| // Wait for any other in-progress rebuild to prevent them overlapping. | |
| await existingUpdateFuture; | |
| } | |
| try { | |
| // This async request is why we need to prevent this code running | |
| // concurrently because they could overlap with each other. | |
| await fetchClientConfiguration(); | |
| await refreshAnalysisRoots(); | |
| } finally { | |
| completer.complete(); | |
| } | |
| } | |
| void _afterOverlayChanged( | |
| String path, | |
| plugin.HasToJson changeForPlugins, { | |
| String? precomputedNewContentForChange, | |
| }) { | |
| for (var driver in driverMap.values) { | |
| driver.changeFile(path); | |
| } | |
| _notifyPluginsOverlayChanged( | |
| path, | |
| changeForPlugins, | |
| precomputedNewContentForChange: precomputedNewContentForChange, | |
| ); | |
| notifyFlutterWidgetDescriptions(path); | |
| } | |
| /// Computes analysis roots for the set of open files. | |
| /// | |
| /// This is used when there are no workspace folders open directly. | |
| Set<String> _getRootsForOpenFiles() { | |
| var openFiles = priorityFiles.toList(); | |
| var roots = locateContextRoots( | |
| includedPaths: openFiles, | |
| resourceProvider: resourceProvider, | |
| ); | |
| var results = <String>{}; | |
| for (var file in openFiles) { | |
| var package = roots | |
| .where((root) => root.isAnalyzed(file)) | |
| .map((root) => root.workspace.findPackageFor(file)?.root) | |
| .nonNulls | |
| .firstOrNull; | |
| if (package != null && !package.isRoot) { | |
| results.add(package.path); | |
| } else { | |
| results.add(file); | |
| } | |
| } | |
| return results; | |
| } | |
| Future<void> _handleNotificationMessage( | |
| NotificationMessage message, | |
| MessageInfo messageInfo, | |
| ) async { | |
| var result = await messageHandler.handleMessage(message, messageInfo); | |
| result.ifError((error) => sendErrorResponse(message, error)); | |
| } | |
| Future<void> _handleRequestMessage( | |
| RequestMessage message, | |
| MessageInfo messageInfo, { | |
| CancelableToken? cancellationToken, | |
| }) async { | |
| var result = await messageHandler.handleMessage( | |
| message, | |
| messageInfo, | |
| cancellationToken: cancellationToken, | |
| ); | |
| result.ifError((error) => sendErrorResponse(message, error)); | |
| result.ifResult( | |
| (result) => messageInfo.performance.run( | |
| 'sendResponse', | |
| (_) => sendResponse( | |
| ResponseMessage( | |
| id: message.id, | |
| result: result, | |
| jsonrpc: jsonRpcVersion, | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| /// Returns whether [filePath] is in a project that can resolve | |
| /// 'package:flutter' libraries. | |
| bool _isInFlutterProject(String filePath) => | |
| getAnalysisDriver(filePath)?.currentSession.uriConverter | |
| .uriToPath(Uri.parse(widgetsUri)) != | |
| null; | |
| void _notifyPluginsOverlayChanged( | |
| String path, | |
| plugin.HasToJson changeForPlugins, { | |
| String? precomputedNewContentForChange, | |
| }) { | |
| pluginManager.setAnalysisUpdateContentParams( | |
| plugin.AnalysisUpdateContentParams({path: changeForPlugins}), | |
| precomputedNewContentForChange: precomputedNewContentForChange, | |
| ); | |
| } | |
| void _onPluginsChanged() { | |
| capabilitiesComputer.performDynamicRegistration(); | |
| } | |
| void _updateDriversAndPluginsPriorityFiles() { | |
| var priorityFilesList = priorityFiles.toList(); | |
| for (var driver in driverMap.values) { | |
| driver.priorityFiles = priorityFilesList; | |
| } | |
| var pluginPriorities = plugin.AnalysisSetPriorityFilesParams( | |
| priorityFilesList, | |
| ); | |
| pluginManager.setAnalysisSetPriorityFilesParams(pluginPriorities); | |
| // Plugins send most of their analysis results via notifications, but with | |
| // LSP we're supposed to have them available per request. Assume that | |
| // we'll only receive requests for files that are currently open. | |
| var pluginSubscriptions = plugin.AnalysisSetSubscriptionsParams({ | |
| for (var service in plugin.AnalysisService.values) | |
| service: priorityFilesList, | |
| }); | |
| pluginManager.setAnalysisSetSubscriptionsParams(pluginSubscriptions); | |
| notificationManager.setSubscriptions({ | |
| for (var service in protocol.AnalysisService.values) | |
| service: priorityFiles, | |
| }); | |
| } | |
| } | |
| class LspInitializationOptions { | |
| final Map<String, Object?> raw; | |
| final String? appHost; | |
| final String? remoteName; | |
| final bool onlyAnalyzeProjectsWithOpenFiles; | |
| final bool suggestFromUnimportedLibraries; | |
| /// Whether closing labels have been enabled by initialization options. | |
| /// | |
| /// This is a legacy option that is still here to support older versions of | |
| /// the Dart-Code VS Code extension prior to v3.144 (releasing ~2026-10-01). | |
| /// Once sufficient time has passed since then (or the number of users on | |
| /// Dart-Code versions earlier than v3.144 is low), this can be removed. | |
| /// | |
| /// The replacement for this (which Dart-Code uses since v3.144 and | |
| /// LSP-over-Legacy always used) is in the client capabilities | |
| /// (`experimental.closingLabels`). | |
| final bool closingLabels; | |
| final bool outline; | |
| final bool flutterOutline; | |
| final int? completionBudgetMilliseconds; | |
| final bool allowOpenUri; | |
| /// Whether the client has expressed an explicit preference for | |
| /// overlapping message handlers. | |
| /// | |
| /// If `null`, the server default will be used. | |
| final bool? allowOverlappingHandlers; | |
| /// A temporary flag passed by Dart-Code to enable using in-editor fixes for | |
| /// the "dart fix" prompt. | |
| /// | |
| /// This allows enabling it once there's been wider testing of the "Fix All in | |
| /// Workspace" command without requiring a new SDK release. Once enabled in | |
| /// Dart-Code, this flag can also be removed here for future SDKs. | |
| final bool useInEditorDartFixPrompt; | |
| factory(Object? options) => LspInitializationOptions._( | |
| options is Map<String, Object?> ? options : const {}, | |
| ); | |
| new _(Map<String, Object?> options) | |
| : raw = options, | |
| appHost = options['appHost'] as String?, | |
| remoteName = options['remoteName'] as String?, | |
| onlyAnalyzeProjectsWithOpenFiles = | |
| options['onlyAnalyzeProjectsWithOpenFiles'] == true, | |
| // suggestFromUnimportedLibraries defaults to true, so must be | |
| // explicitly passed as false to disable. | |
| suggestFromUnimportedLibraries = | |
| options['suggestFromUnimportedLibraries'] != false, | |
| closingLabels = options['closingLabels'] == true, | |
| outline = options['outline'] == true, | |
| flutterOutline = options['flutterOutline'] == true, | |
| completionBudgetMilliseconds = | |
| options['completionBudgetMilliseconds'] as int?, | |
| allowOpenUri = options['allowOpenUri'] == true, | |
| allowOverlappingHandlers = options['allowOverlappingHandlers'] as bool?, | |
| useInEditorDartFixPrompt = options['useInEditorDartFixPrompt'] == true; | |
| } | |
| class LspServerContextManagerCallbacks | |
| extends CommonServerContextManagerCallbacks { | |
| @override | |
| final LspAnalysisServer analysisServer; | |
| new(this.analysisServer, super.resourceProvider); | |
| @override | |
| void afterContextsCreated() { | |
| super.afterContextsCreated(); | |
| analysisServer.contextBuilds++; | |
| } | |
| @override | |
| void flushResults(List<String> files) { | |
| if (analysisServer.isShuttingDown) { | |
| // When the server is shutting down, analysis contexts will be destroyed. | |
| // Destroying analysis contexts usually flushes all diagnostics, however | |
| // we don't want this during a shutdown because for one-shot clients like | |
| // 'dart analyze', it will look like the diagnostics went away. Instead, | |
| // clients should handle cleaning up diagnostics for a shut-down server. | |
| return; | |
| } | |
| for (var file in files) { | |
| analysisServer.publishDiagnostics(file, []); | |
| } | |
| } | |
| @override | |
| void handleResolvedUnitResult(ResolvedUnitResult result) { | |
| var path = result.path; | |
| var unit = result.unit; | |
| if (analysisServer.shouldSendClosingLabelsFor(path)) { | |
| var labels = DartUnitClosingLabelsComputer( | |
| result.lineInfo, | |
| unit, | |
| ).compute().map((l) => toClosingLabel(result.lineInfo, l)).toList(); | |
| analysisServer.publishLspClosingLabels(path, labels); | |
| } | |
| if (analysisServer.shouldSendOutlineFor(path)) { | |
| var outline = DartUnitOutlineComputer( | |
| result, | |
| withBasicFlutter: true, | |
| ).compute(); | |
| var lspOutline = toOutline(result.lineInfo, outline); | |
| analysisServer.publishOutline(path, lspOutline); | |
| } | |
| if (analysisServer.shouldSendFlutterOutlineFor(path)) { | |
| var outline = FlutterOutlineComputer(result).compute(); | |
| var lspOutline = toFlutterOutline(result.lineInfo, outline); | |
| analysisServer.publishFlutterOutline(path, lspOutline); | |
| } | |
| } | |
| @override | |
| void recordAnalysisErrors(String path, List<protocol.AnalysisError> errors) { | |
| super.recordAnalysisErrors(path, errors.where(_shouldSendError).toList()); | |
| } | |
| bool _shouldSendError(protocol.AnalysisError error) { | |
| // Non-TODOs are always shown. | |
| if (error.type.name != engine.DiagnosticType.TODO.name) { | |
| return true; | |
| } | |
| // TODOs that are upgraded from INFO are always shown. | |
| if (error.severity.name != engine.DiagnosticSeverity.INFO.name) { | |
| return true; | |
| } | |
| // Otherwise, show TODOs based on client configuration (either showing all, | |
| // or specific types of TODOs). | |
| if (analysisServer.lspClientConfiguration.global.showAllTodos) { | |
| return true; | |
| } | |
| return analysisServer.lspClientConfiguration.global.showTodoTypes.contains( | |
| error.code.toUpperCase(), | |
| ); | |
| } | |
| } | |
| extension on Completer<void> { | |
| void setComplete() { | |
| if (isCompleted) { | |
| return; | |
| } | |
| complete(); | |
| } | |
| } |