| // 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 'dart:collection'; |
| |
| import 'package:analysis_server/lsp_protocol/protocol_custom_generated.dart'; |
| import 'package:analysis_server/lsp_protocol/protocol_generated.dart'; |
| import 'package:analysis_server/lsp_protocol/protocol_special.dart'; |
| import 'package:analysis_server/protocol/protocol_generated.dart' as protocol; |
| import 'package:analysis_server/src/analysis_server.dart'; |
| import 'package:analysis_server/src/analysis_server_abstract.dart'; |
| import 'package:analysis_server/src/collections.dart'; |
| import 'package:analysis_server/src/computer/computer_closingLabels.dart'; |
| import 'package:analysis_server/src/computer/computer_outline.dart'; |
| import 'package:analysis_server/src/context_manager.dart'; |
| import 'package:analysis_server/src/domain_completion.dart' |
| show CompletionDomainHandler; |
| import 'package:analysis_server/src/flutter/flutter_outline_computer.dart'; |
| import 'package:analysis_server/src/lsp/channel/lsp_channel.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/plugin/plugin_manager.dart'; |
| import 'package:analysis_server/src/protocol_server.dart' as protocol; |
| import 'package:analysis_server/src/server/crash_reporting_attachments.dart'; |
| import 'package:analysis_server/src/server/diagnostic_server.dart'; |
| import 'package:analysis_server/src/server/error_notifier.dart'; |
| import 'package:analysis_server/src/services/completion/completion_performance.dart' |
| show CompletionPerformance; |
| import 'package:analysis_server/src/services/refactoring/refactoring.dart'; |
| import 'package:analyzer/error/error.dart'; |
| import 'package:analyzer/exception/exception.dart'; |
| import 'package:analyzer/file_system/file_system.dart'; |
| import 'package:analyzer/instrumentation/instrumentation.dart'; |
| import 'package:analyzer/source/line_info.dart'; |
| import 'package:analyzer/src/context/builder.dart'; |
| import 'package:analyzer/src/context/context_root.dart'; |
| import 'package:analyzer/src/dart/analysis/driver.dart' as nd; |
| import 'package:analyzer/src/dart/analysis/status.dart' as nd; |
| import 'package:analyzer/src/generated/engine.dart'; |
| import 'package:analyzer/src/generated/sdk.dart'; |
| import 'package:analyzer_plugin/protocol/protocol_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:path/path.dart'; |
| import 'package:watcher/watcher.dart'; |
| |
| /// Instances of the class [LspAnalysisServer] implement an LSP-based server |
| /// that listens on a [CommunicationChannel] for LSP messages and processes |
| /// them. |
| class LspAnalysisServer extends AbstractAnalysisServer { |
| /// The capabilities of the LSP client. Will be null prior to initialization. |
| ClientCapabilities _clientCapabilities; |
| |
| /// 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. |
| final LspClientConfiguration clientConfiguration = LspClientConfiguration(); |
| |
| /// The channel from which messages are received and to which responses should |
| /// be sent. |
| final LspServerCommunicationChannel channel; |
| |
| /// The workspace for rename refactorings. Should be accessed through the |
| /// refactoringWorkspace getter to be automatically created (lazily). |
| RefactoringWorkspace _refactoringWorkspace; |
| |
| /// 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, VersionedTextDocumentIdentifier> documentVersions = {}; |
| |
| ServerStateMessageHandler messageHandler; |
| |
| int nextRequestId = 1; |
| |
| 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; |
| ServerCapabilitiesComputer capabilitiesComputer; |
| |
| LspPerformance performanceStats = LspPerformance(); |
| |
| /// Whether or not the server is controlling the shutdown and will exit |
| /// automatically. |
| bool willExit = false; |
| |
| StreamSubscription _pluginChangeSubscription; |
| |
| /// Temporary analysis roots for open files. |
| /// |
| /// When a file is opened and there is no driver available (for example no |
| /// folder was opened in the editor, so the set of analysis roots is empty) |
| /// we add temporary roots for the project (or containing) folder. When the |
| /// file is closed, it is removed from this map and if no other open file |
| /// uses that root, it will be removed from the set of analysis roots. |
| /// |
| /// key: file path of the open file |
| /// value: folder to be used as a root. |
| final _temporaryAnalysisRoots = <String, String>{}; |
| |
| /// The set of analysis roots explicitly added to the workspace. |
| final _explicitAnalysisRoots = HashSet<String>(); |
| |
| /// A progress reporter for analysis status. |
| ProgressReporter analyzingProgressReporter; |
| |
| /// Initialize a newly created server to send and receive messages to the |
| /// given [channel]. |
| LspAnalysisServer( |
| this.channel, |
| ResourceProvider baseResourceProvider, |
| AnalysisServerOptions options, |
| DartSdkManager sdkManager, |
| CrashReportingAttachmentsBuilder crashReportingAttachmentsBuilder, |
| InstrumentationService instrumentationService, { |
| DiagnosticServer diagnosticServer, |
| }) : super( |
| options, |
| sdkManager, |
| diagnosticServer, |
| crashReportingAttachmentsBuilder, |
| baseResourceProvider, |
| instrumentationService, |
| LspNotificationManager(channel, baseResourceProvider.pathContext), |
| ) { |
| notificationManager.server = this; |
| messageHandler = UninitializedStateMessageHandler(this); |
| capabilitiesComputer = ServerCapabilitiesComputer(this); |
| |
| final contextManagerCallbacks = |
| LspServerContextManagerCallbacks(this, resourceProvider); |
| contextManager.callbacks = contextManagerCallbacks; |
| |
| analysisDriverScheduler.status.listen(sendStatusNotification); |
| analysisDriverScheduler.start(); |
| |
| channel.listen(handleMessage, onDone: done, onError: socketError); |
| _pluginChangeSubscription = |
| pluginManager.pluginsChanged.listen((_) => _onPluginsChanged()); |
| } |
| |
| /// The capabilities of the LSP client. Will be null prior to initialization. |
| ClientCapabilities get clientCapabilities => _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. |
| LspInitializationOptions get initializationOptions => _initializationOptions; |
| |
| @override |
| LspNotificationManager get notificationManager => super.notificationManager; |
| |
| @override |
| set pluginManager(PluginManager value) { |
| // we exchange the plugin manager in tests |
| super.pluginManager = value; |
| _pluginChangeSubscription?.cancel(); |
| |
| _pluginChangeSubscription = |
| pluginManager.pluginsChanged.listen((_) => _onPluginsChanged()); |
| } |
| |
| RefactoringWorkspace get refactoringWorkspace => _refactoringWorkspace ??= |
| RefactoringWorkspace(driverMap.values, searchEngine); |
| |
| void addPriorityFile(String path) { |
| final didAdd = priorityFiles.add(path); |
| assert(didAdd); |
| if (didAdd) { |
| _updateDriversAndPluginsPriorityFiles(); |
| } |
| } |
| |
| /// Adds a temporary analysis root for an open file. |
| void addTemporaryAnalysisRoot(String filePath, String folderPath) { |
| _temporaryAnalysisRoots[filePath] = folderPath; |
| _refreshAnalysisRoots(); |
| } |
| |
| /// The socket from which messages are being read has been closed. |
| void done() {} |
| |
| /// Fetches configuration from the client (if supported) and then sends |
| /// register/unregister requests for any supported/enabled dynamic registrations. |
| Future<void> fetchClientConfigurationAndPerformDynamicRegistration() async { |
| if (clientCapabilities.workspace?.configuration ?? false) { |
| // 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"). |
| final response = await sendRequest( |
| Method.workspace_configuration, |
| ConfigurationParams(items: [ |
| ConfigurationItem(section: 'dart'), |
| ])); |
| |
| final result = response.result; |
| |
| // Expect the result to be a single list (to match the single |
| // ConfigurationItem we requested above) and that it 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 each group appropriately. |
| if (result != null && |
| result is List<dynamic> && |
| result.length == 1 && |
| result.first is Map<String, dynamic>) { |
| final newConfig = result.first; |
| final refreshRoots = |
| clientConfiguration.affectsAnalysisRoots(newConfig); |
| |
| clientConfiguration.replace(newConfig); |
| |
| if (refreshRoots) { |
| _refreshAnalysisRoots(); |
| } |
| } |
| } |
| |
| // Client config can affect capabilities, so this should only be done after |
| // we have the initial/updated config. |
| capabilitiesComputer.performDynamicRegistration(); |
| } |
| |
| /// Return the LineInfo 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. |
| LineInfo getLineInfo(String path) { |
| return getAnalysisDriver(path)?.getFileSync(path)?.lineInfo; |
| } |
| |
| /// Gets the version of a document known to the server, returning a |
| /// [VersionedTextDocumentIdentifier] with a version of `null` if the document |
| /// version is not known. |
| VersionedTextDocumentIdentifier getVersionedDocumentIdentifier(String path) { |
| return documentVersions[path] ?? |
| VersionedTextDocumentIdentifier(uri: Uri.file(path).toString()); |
| } |
| |
| void handleClientConnection( |
| ClientCapabilities capabilities, dynamic initializationOptions) { |
| _clientCapabilities = capabilities; |
| _initializationOptions = LspInitializationOptions(initializationOptions); |
| |
| performanceAfterStartup = ServerPerformance(); |
| performance = performanceAfterStartup; |
| } |
| |
| /// 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 string because it should match a request we sent to the |
| // client (and we always use numeric IDs for outgoing requests). |
| message.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. |
| final 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. |
| void handleMessage(Message message) { |
| performance.logRequestTiming(null); |
| runZonedGuarded(() async { |
| try { |
| if (message is ResponseMessage) { |
| handleClientResponse(message); |
| } else if (message is RequestMessage) { |
| final result = await messageHandler.handleMessage(message); |
| if (result.isError) { |
| sendErrorResponse(message, result.error); |
| } else { |
| channel.sendResponse(ResponseMessage( |
| id: message.id, |
| result: result.result, |
| jsonrpc: jsonRpcVersion)); |
| } |
| } else if (message is NotificationMessage) { |
| final result = await messageHandler.handleMessage(message); |
| if (result.isError) { |
| sendErrorResponse(message, result.error); |
| } |
| } else { |
| showErrorMessageToUser('Unknown message type'); |
| } |
| } catch (error, stackTrace) { |
| final 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); |
| } |
| }, socketError); |
| } |
| |
| /// Returns `true` if the [file] with the given absolute path is included |
| /// in an analysis root and not excluded. |
| bool isAnalyzedFile(String file) { |
| return contextManager.isInAnalysisRoot(file) && |
| // Dot folders are not analyzed (skipped over in _handleWatchEventImpl) |
| !contextManager.isContainedInDotFolder(file) && |
| !contextManager.isIgnored(file); |
| } |
| |
| /// 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, 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, exception, stackTrace) { |
| var fullMessage = message; |
| if (exception is CaughtException) { |
| stackTrace ??= exception.stackTrace; |
| fullMessage = '$fullMessage: ${exception.exception}'; |
| } else if (exception != null) { |
| fullMessage = '$fullMessage: $exception'; |
| } |
| |
| final fullError = |
| stackTrace == null ? fullMessage : '$fullMessage\n$stackTrace'; |
| |
| // 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 is StackTrace ? stackTrace : null, |
| false, |
| )); |
| |
| AnalysisEngine.instance.instrumentationService.logException( |
| FatalException( |
| message, |
| exception, |
| stackTrace, |
| ), |
| null, |
| crashReportingAttachmentsBuilder.forException(exception), |
| ); |
| } |
| |
| void onOverlayCreated(String path, String content) { |
| resourceProvider.setOverlay(path, |
| content: content, modificationStamp: overlayModificationStamp++); |
| |
| _afterOverlayChanged(path, plugin.AddContentOverlay(content)); |
| } |
| |
| void onOverlayDestroyed(String path) { |
| resourceProvider.removeOverlay(path); |
| |
| _afterOverlayChanged(path, plugin.RemoveContentOverlay()); |
| } |
| |
| /// Updates an overlay on [path] by applying the [edits] to the current |
| /// overlay. |
| /// |
| /// If the result of applying the edits is already known, [newContent] can be |
| /// set to avoid doing that calculation twice. |
| void onOverlayUpdated(String path, Iterable<plugin.SourceEdit> edits, |
| {String newContent}) { |
| assert(resourceProvider.hasOverlay(path)); |
| if (newContent == null) { |
| final oldContent = resourceProvider.getFile(path).readAsStringSync(); |
| newContent = plugin.applySequenceOfEdits(oldContent, edits); |
| } |
| |
| resourceProvider.setOverlay(path, |
| content: newContent, modificationStamp: overlayModificationStamp++); |
| |
| _afterOverlayChanged(path, plugin.ChangeContentOverlay(edits)); |
| } |
| |
| void publishClosingLabels(String path, List<ClosingLabel> labels) { |
| final params = PublishClosingLabelsParams( |
| uri: Uri.file(path).toString(), labels: labels); |
| final message = NotificationMessage( |
| method: CustomMethods.PublishClosingLabels, |
| params: params, |
| jsonrpc: jsonRpcVersion, |
| ); |
| sendNotification(message); |
| } |
| |
| void publishDiagnostics(String path, List<Diagnostic> errors) { |
| final params = PublishDiagnosticsParams( |
| uri: Uri.file(path).toString(), diagnostics: errors); |
| final message = NotificationMessage( |
| method: Method.textDocument_publishDiagnostics, |
| params: params, |
| jsonrpc: jsonRpcVersion, |
| ); |
| sendNotification(message); |
| } |
| |
| void publishFlutterOutline(String path, FlutterOutline outline) { |
| final params = PublishFlutterOutlineParams( |
| uri: Uri.file(path).toString(), outline: outline); |
| final message = NotificationMessage( |
| method: CustomMethods.PublishFlutterOutline, |
| params: params, |
| jsonrpc: jsonRpcVersion, |
| ); |
| sendNotification(message); |
| } |
| |
| void publishOutline(String path, Outline outline) { |
| final params = |
| PublishOutlineParams(uri: Uri.file(path).toString(), outline: outline); |
| final message = NotificationMessage( |
| method: CustomMethods.PublishOutline, |
| params: params, |
| jsonrpc: jsonRpcVersion, |
| ); |
| sendNotification(message); |
| } |
| |
| void removePriorityFile(String path) { |
| final didRemove = priorityFiles.remove(path); |
| assert(didRemove); |
| if (didRemove) { |
| _updateDriversAndPluginsPriorityFiles(); |
| } |
| } |
| |
| /// Removes any temporary analysis root for a file that was closed. |
| void removeTemporaryAnalysisRoot(String filePath) { |
| _temporaryAnalysisRoots.remove(filePath); |
| _refreshAnalysisRoots(); |
| } |
| |
| void sendErrorResponse(Message message, ResponseError error) { |
| if (message is RequestMessage) { |
| channel.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); |
| |
| final message = 'An unrecoverable error occurred.'; |
| logErrorToClient( |
| '$message\n\n${error.message}\n\n${error.code}\n\n${error.data}'); |
| |
| shutdown(); |
| } |
| } |
| |
| /// Send the given [notification] to the client. |
| void sendNotification(NotificationMessage notification) { |
| channel.sendNotification(notification); |
| } |
| |
| /// Send the given [request] to the client and wait for a response. Completes |
| /// with the raw [ResponseMessage] which could be an error response. |
| Future<ResponseMessage> sendRequest(Method method, Object params) { |
| final requestId = nextRequestId++; |
| final completer = Completer<ResponseMessage>(); |
| completers[requestId] = completer; |
| |
| channel.sendRequest(RequestMessage( |
| id: Either2<num, 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); |
| } |
| |
| @override |
| void sendServerErrorNotification(String message, exception, stackTrace, |
| {bool fatal = false}) { |
| message = exception == null ? message : '$message: $exception'; |
| |
| // Show message (without stack) to the user. |
| showErrorMessageToUser(message); |
| |
| logException(message, exception, stackTrace); |
| } |
| |
| /// Send status notification to the client. The state of analysis is given by |
| /// the [status] information. |
| Future<void> sendStatusNotification(nd.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. |
| if (clientCapabilities.window?.workDoneProgress != true) { |
| channel.sendNotification(NotificationMessage( |
| method: CustomMethods.AnalyzerStatus, |
| params: AnalyzerStatusParams(isAnalyzing: status.isAnalyzing), |
| jsonrpc: jsonRpcVersion, |
| )); |
| return; |
| } |
| |
| if (status.isAnalyzing) { |
| analyzingProgressReporter ??= |
| ProgressReporter.serverCreated(this, analyzingProgressToken) |
| ..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. |
| return initializationOptions.closingLabels && |
| priorityFiles.contains(file) && |
| contextManager.isInAnalysisRoot(file); |
| } |
| |
| /// Returns `true` if errors should be reported for [file] with the given |
| /// absolute path. |
| bool shouldSendErrorsNotificationFor(String file) { |
| return isAnalyzedFile(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 && priorityFiles.contains(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 && 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, message: message), |
| jsonrpc: jsonRpcVersion, |
| )); |
| } |
| |
| /// Shows the user a prompt with some actions to select using ShowMessageRequest. |
| Future<MessageActionItem> showUserPrompt( |
| MessageType type, String message, List<MessageActionItem> actions) async { |
| final response = await sendRequest( |
| Method.window_showMessageRequest, |
| ShowMessageRequestParams(type: type, message: message, actions: actions), |
| ); |
| |
| return MessageActionItem.fromJson(response.result); |
| } |
| |
| Future<void> shutdown() { |
| // Defer closing the channel so that the shutdown response can be sent and |
| // logged. |
| Future(() { |
| channel.close(); |
| }); |
| _pluginChangeSubscription?.cancel(); |
| |
| return Future.value(); |
| } |
| |
| /// There was an error related to the socket from which messages are being |
| /// read. |
| void socketError(error, stack) { |
| // Don't send to instrumentation service; not an internal error. |
| sendServerErrorNotification('Socket error', error, stack); |
| } |
| |
| void updateAnalysisRoots(List<String> addedPaths, List<String> removedPaths) { |
| // TODO(dantup): This is currently case-sensitive! |
| |
| _explicitAnalysisRoots |
| ..addAll(addedPaths ?? const []) |
| ..removeAll(removedPaths ?? const []); |
| |
| _refreshAnalysisRoots(); |
| } |
| |
| void _afterOverlayChanged(String path, dynamic changeForPlugins) { |
| driverMap.values.forEach((driver) => driver.changeFile(path)); |
| pluginManager.setAnalysisUpdateContentParams( |
| plugin.AnalysisUpdateContentParams({path: changeForPlugins}), |
| ); |
| |
| notifyDeclarationsTracker(path); |
| notifyFlutterWidgetDescriptions(path); |
| } |
| |
| void _onPluginsChanged() { |
| capabilitiesComputer.performDynamicRegistration(); |
| } |
| |
| void _refreshAnalysisRoots() { |
| // Always include any temporary analysis roots for open files. |
| final includedPaths = HashSet<String>.of(_explicitAnalysisRoots) |
| ..addAll(_temporaryAnalysisRoots.values) |
| ..toList(); |
| |
| final excludedPaths = clientConfiguration.analysisExcludedFolders |
| .expand((excludePath) => 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. |
| : _explicitAnalysisRoots.map((root) => join(root, excludePath))) |
| .toList(); |
| |
| declarationsTracker?.discardContexts(); |
| notificationManager.setAnalysisRoots(includedPaths.toList(), excludedPaths); |
| contextManager.setRoots(includedPaths.toList(), excludedPaths); |
| addContextsToDeclarationsTracker(); |
| } |
| |
| void _updateDriversAndPluginsPriorityFiles() { |
| final priorityFilesList = priorityFiles.toList(); |
| driverMap.values.forEach((driver) { |
| driver.priorityFiles = priorityFilesList; |
| }); |
| |
| final pluginPriorities = |
| plugin.AnalysisSetPriorityFilesParams(priorityFilesList); |
| pluginManager.setAnalysisSetPriorityFilesParams(pluginPriorities); |
| |
| // Plugins send most of their analysis results via notifications, but with |
| // LSP we're supposed to have them available per request. Assume that we'll |
| // only receive requests for files that are currently open. |
| final pluginSubscriptions = plugin.AnalysisSetSubscriptionsParams({ |
| for (final service in plugin.AnalysisService.VALUES) |
| service: priorityFilesList, |
| }); |
| pluginManager.setAnalysisSetSubscriptionsParams(pluginSubscriptions); |
| |
| notificationManager.setSubscriptions({ |
| for (final service in protocol.AnalysisService.VALUES) |
| service: priorityFiles |
| }); |
| } |
| } |
| |
| class LspInitializationOptions { |
| final bool onlyAnalyzeProjectsWithOpenFiles; |
| final bool suggestFromUnimportedLibraries; |
| final bool closingLabels; |
| final bool outline; |
| final bool flutterOutline; |
| |
| LspInitializationOptions(dynamic options) |
| : onlyAnalyzeProjectsWithOpenFiles = options != null && |
| options['onlyAnalyzeProjectsWithOpenFiles'] == true, |
| // suggestFromUnimportedLibraries defaults to true, so must be |
| // explicitly passed as false to disable. |
| suggestFromUnimportedLibraries = options == null || |
| options['suggestFromUnimportedLibraries'] != false, |
| closingLabels = options != null && options['closingLabels'] == true, |
| outline = options != null && options['outline'] == true, |
| flutterOutline = options != null && options['flutterOutline'] == true; |
| } |
| |
| class LspPerformance { |
| /// A list of code completion performance measurements for the latest |
| /// completion operation up to [performanceListMaxLength] measurements. |
| final RecentBuffer<CompletionPerformance> completion = |
| RecentBuffer<CompletionPerformance>( |
| CompletionDomainHandler.performanceListMaxLength); |
| } |
| |
| class LspServerContextManagerCallbacks extends ContextManagerCallbacks { |
| // TODO(dantup): Lots of copy/paste from the Analysis Server one here. |
| |
| final LspAnalysisServer analysisServer; |
| |
| /// The [ResourceProvider] by which paths are converted into [Resource]s. |
| final ResourceProvider resourceProvider; |
| |
| LspServerContextManagerCallbacks(this.analysisServer, this.resourceProvider); |
| |
| @override |
| LspNotificationManager get notificationManager => |
| analysisServer.notificationManager; |
| |
| @override |
| nd.AnalysisDriver addAnalysisDriver(Folder folder, ContextRoot contextRoot) { |
| var builder = createContextBuilder(folder); |
| var analysisDriver = builder.buildDriver(contextRoot); |
| final textDocumentCapabilities = |
| analysisServer.clientCapabilities?.textDocument; |
| final supportedDiagnosticTags = HashSet<DiagnosticTag>.of( |
| textDocumentCapabilities?.publishDiagnostics?.tagSupport?.valueSet ?? |
| []); |
| analysisDriver.results.listen((result) { |
| var path = result.path; |
| if (analysisServer.shouldSendErrorsNotificationFor(path)) { |
| final serverErrors = protocol.mapEngineErrors( |
| result, |
| result.errors |
| .where((e) => e.errorCode.type != ErrorType.TODO) |
| .toList(), |
| (result, error, [severity]) => toDiagnostic( |
| result, |
| error, |
| supportedTags: supportedDiagnosticTags, |
| errorSeverity: severity, |
| )); |
| |
| analysisServer.publishDiagnostics(result.path, serverErrors); |
| } |
| if (result.unit != null) { |
| if (analysisServer.shouldSendClosingLabelsFor(path)) { |
| final labels = |
| DartUnitClosingLabelsComputer(result.lineInfo, result.unit) |
| .compute() |
| .map((l) => toClosingLabel(result.lineInfo, l)) |
| .toList(); |
| |
| analysisServer.publishClosingLabels(result.path, labels); |
| } |
| if (analysisServer.shouldSendOutlineFor(path)) { |
| final outline = DartUnitOutlineComputer( |
| result, |
| withBasicFlutter: true, |
| ).compute(); |
| final lspOutline = toOutline(result.lineInfo, outline); |
| analysisServer.publishOutline(result.path, lspOutline); |
| } |
| if (analysisServer.shouldSendFlutterOutlineFor(path)) { |
| final outline = FlutterOutlineComputer(result).compute(); |
| final lspOutline = toFlutterOutline(result.lineInfo, outline); |
| analysisServer.publishFlutterOutline(result.path, lspOutline); |
| } |
| } |
| }); |
| analysisDriver.exceptions.listen(analysisServer.logExceptionResult); |
| analysisDriver.priorityFiles = analysisServer.priorityFiles.toList(); |
| analysisServer.driverMap[folder] = analysisDriver; |
| return analysisDriver; |
| } |
| |
| @override |
| void afterWatchEvent(WatchEvent event) { |
| // TODO: implement afterWatchEvent |
| } |
| |
| @override |
| void analysisOptionsUpdated(nd.AnalysisDriver driver) { |
| // TODO: implement analysisOptionsUpdated |
| } |
| |
| @override |
| void applyChangesToContext(Folder contextFolder, ChangeSet changeSet) { |
| var analysisDriver = analysisServer.driverMap[contextFolder]; |
| if (analysisDriver != null) { |
| changeSet.addedFiles.forEach((path) { |
| analysisDriver.addFile(path); |
| }); |
| changeSet.changedFiles.forEach((path) { |
| analysisDriver.changeFile(path); |
| }); |
| changeSet.removedFiles.forEach((path) { |
| analysisDriver.removeFile(path); |
| }); |
| } |
| } |
| |
| @override |
| void applyFileRemoved(nd.AnalysisDriver driver, String file) { |
| driver.removeFile(file); |
| analysisServer.publishDiagnostics(file, []); |
| } |
| |
| @override |
| void broadcastWatchEvent(WatchEvent event) { |
| analysisServer.notifyDeclarationsTracker(event.path); |
| analysisServer.notifyFlutterWidgetDescriptions(event.path); |
| analysisServer.pluginManager.broadcastWatchEvent(event); |
| } |
| |
| @override |
| ContextBuilder createContextBuilder(Folder folder) { |
| var builderOptions = ContextBuilderOptions(); |
| var builder = ContextBuilder( |
| resourceProvider, analysisServer.sdkManager, null, |
| options: builderOptions); |
| builder.analysisDriverScheduler = analysisServer.analysisDriverScheduler; |
| builder.performanceLog = analysisServer.analysisPerformanceLogger; |
| builder.byteStore = analysisServer.byteStore; |
| builder.enableIndex = true; |
| return builder; |
| } |
| |
| @override |
| void removeContext(Folder folder, List<String> flushedFiles) { |
| var driver = analysisServer.driverMap.remove(folder); |
| // Flush any errors for these files that the client may be displaying. |
| flushedFiles |
| ?.forEach((path) => analysisServer.publishDiagnostics(path, const [])); |
| driver.dispose(); |
| } |
| } |