blob: 3c67aab534907902198a7e69dc45d4c67bd2bfb2 [file] [log] [blame]
// 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].
ResourceProvider baseResourceProvider,
AnalysisServerOptions options,
DartSdkManager sdkManager,
CrashReportingAttachmentsBuilder crashReportingAttachmentsBuilder,
InstrumentationService instrumentationService, {
DiagnosticServer diagnosticServer,
}) : super(
LspNotificationManager(channel, baseResourceProvider.pathContext),
) {
notificationManager.server = this;
messageHandler = UninitializedStateMessageHandler(this);
capabilitiesComputer = ServerCapabilitiesComputer(this);
final contextManagerCallbacks =
LspServerContextManagerCallbacks(this, resourceProvider);
contextManager.callbacks = contextManagerCallbacks;
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;
LspNotificationManager get notificationManager => super.notificationManager;
set pluginManager(PluginManager value) {
// we exchange the plugin manager in tests
super.pluginManager = value;
_pluginChangeSubscription =
pluginManager.pluginsChanged.listen((_) => _onPluginsChanged());
RefactoringWorkspace get refactoringWorkspace => _refactoringWorkspace ??=
RefactoringWorkspace(driverMap.values, searchEngine);
void addPriorityFile(String path) {
final didAdd = priorityFiles.add(path);
if (didAdd) {
/// Adds a temporary analysis root for an open file.
void addTemporaryAnalysisRoot(String filePath, String folderPath) {
_temporaryAnalysisRoots[filePath] = folderPath;
/// 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(
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 =
if (refreshRoots) {
// Client config can affect capabilities, so this should only be done after
// we have the initial/updated config.
/// 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).
(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 {
(stringID) {
showErrorMessageToUser('Unexpected String ID for response $stringID');
/// Handle a [message] that was read from the communication channel.
void handleMessage(Message message) {
runZonedGuarded(() async {
try {
if (message is ResponseMessage) {
} else if (message is RequestMessage) {
final result = await messageHandler.handleMessage(message);
if (result.isError) {
sendErrorResponse(message, result.error);
} else {
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 is RequestMessage
? 'An error occurred while handling ${message.method} request'
: message is NotificationMessage
? 'An error occurred while handling ${message.method} notification'
: 'Unknown message type';
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) &&
/// Logs the error on the client using window/logMessage.
void logErrorToClient(String message) {
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).
// remember the last few exceptions
stackTrace is StackTrace ? stackTrace : null,
void onOverlayCreated(String path, String content) {
content: content, modificationStamp: overlayModificationStamp++);
_afterOverlayChanged(path, plugin.AddContentOverlay(content));
void onOverlayDestroyed(String 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}) {
if (newContent == null) {
final oldContent = resourceProvider.getFile(path).readAsStringSync();
newContent = plugin.applySequenceOfEdits(oldContent, edits);
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,
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,
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,
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,
void removePriorityFile(String path) {
final didRemove = priorityFiles.remove(path);
if (didRemove) {
/// Removes any temporary analysis root for a file that was closed.
void removeTemporaryAnalysisRoot(String filePath) {
void sendErrorResponse(Message message, ResponseError error) {
if (message is RequestMessage) {
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.
} else {
// For notifications where we couldn't respond with an error, send it as
// show instead of log.
// 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.';
/// Send the given [notification] to the client.
void sendNotification(NotificationMessage 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;
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) {
void sendServerErrorNotification(String message, exception, stackTrace,
{bool fatal = false}) {
message = exception == null ? message : '$message: $exception';
// Show message (without stack) to the user.
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) {
method: CustomMethods.AnalyzerStatus,
params: AnalyzerStatusParams(isAnalyzing: status.isAnalyzing),
jsonrpc: jsonRpcVersion,
if (status.isAnalyzing) {
analyzingProgressReporter ??=
ProgressReporter.serverCreated(this, analyzingProgressToken)
} 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) &&
/// 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) {
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(
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(() {
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!
..addAll(addedPaths ?? const [])
..removeAll(removedPaths ?? const []);
void _afterOverlayChanged(String path, dynamic changeForPlugins) {
driverMap.values.forEach((driver) => driver.changeFile(path));
plugin.AnalysisUpdateContentParams({path: changeForPlugins}),
void _onPluginsChanged() {
void _refreshAnalysisRoots() {
// Always include any temporary analysis roots for open files.
final includedPaths = HashSet<String>.of(_explicitAnalysisRoots)
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.
: => join(root, excludePath)))
notificationManager.setAnalysisRoots(includedPaths.toList(), excludedPaths);
contextManager.setRoots(includedPaths.toList(), excludedPaths);
void _updateDriversAndPluginsPriorityFiles() {
final priorityFilesList = priorityFiles.toList();
driverMap.values.forEach((driver) {
driver.priorityFiles = priorityFilesList;
final 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,
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 =
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);
LspNotificationManager get notificationManager =>
nd.AnalysisDriver addAnalysisDriver(Folder folder, ContextRoot contextRoot) {
var builder = createContextBuilder(folder);
var analysisDriver = builder.buildDriver(contextRoot);
final textDocumentCapabilities =
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(
.where((e) => e.errorCode.type != ErrorType.TODO)
(result, error, [severity]) => toDiagnostic(
supportedTags: supportedDiagnosticTags,
errorSeverity: severity,
analysisServer.publishDiagnostics(result.path, serverErrors);
if (result.unit != null) {
if (analysisServer.shouldSendClosingLabelsFor(path)) {
final labels =
DartUnitClosingLabelsComputer(result.lineInfo, result.unit)
.map((l) => toClosingLabel(result.lineInfo, l))
analysisServer.publishClosingLabels(result.path, labels);
if (analysisServer.shouldSendOutlineFor(path)) {
final outline = DartUnitOutlineComputer(
withBasicFlutter: true,
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.priorityFiles = analysisServer.priorityFiles.toList();
analysisServer.driverMap[folder] = analysisDriver;
return analysisDriver;
void afterWatchEvent(WatchEvent event) {
// TODO: implement afterWatchEvent
void analysisOptionsUpdated(nd.AnalysisDriver driver) {
// TODO: implement analysisOptionsUpdated
void applyChangesToContext(Folder contextFolder, ChangeSet changeSet) {
var analysisDriver = analysisServer.driverMap[contextFolder];
if (analysisDriver != null) {
changeSet.addedFiles.forEach((path) {
changeSet.changedFiles.forEach((path) {
changeSet.removedFiles.forEach((path) {
void applyFileRemoved(nd.AnalysisDriver driver, String file) {
analysisServer.publishDiagnostics(file, []);
void broadcastWatchEvent(WatchEvent event) {
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;
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.
?.forEach((path) => analysisServer.publishDiagnostics(path, const []));