| // Copyright (c) 2015, 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. | |
| /// Support for interacting with an analysis server that is running in a | |
| /// separate process. | |
| library; | |
| import 'dart:async'; | |
| import 'dart:collection'; | |
| import 'dart:convert' hide JsonDecoder; | |
| import 'dart:io'; | |
| import 'dart:math' as math; | |
| import 'package:analysis_server/protocol/protocol.dart'; | |
| import 'package:analysis_server/protocol/protocol_generated.dart'; | |
| import 'package:analyzer_plugin/protocol/protocol_common.dart'; | |
| import 'package:path/path.dart' as path; | |
| import 'logger.dart'; | |
| /// Return the current time expressed as milliseconds since the epoch. | |
| int get currentTime => DateTime.now().millisecondsSinceEpoch; | |
| /// ??? | |
| class ErrorMap { | |
| /// A table mapping file paths to the errors associated with that file. | |
| final Map<String, List<AnalysisError>> pathMap = | |
| HashMap<String, List<AnalysisError>>(); | |
| /// Initialize a newly created error map. | |
| new(); | |
| /// Initialize a newly created error map to contain the same mapping as the | |
| /// given [errorMap]. | |
| new from(ErrorMap errorMap) { | |
| pathMap.addAll(errorMap.pathMap); | |
| } | |
| void operator []=(String filePath, List<AnalysisError> errors) { | |
| pathMap[filePath] = errors; | |
| } | |
| } | |
| /// Data that has been collected about a request sent to the server. | |
| class RequestData { | |
| /// The unique id of the request. | |
| final String id; | |
| /// The method that was requested. | |
| final String method; | |
| /// The request parameters, or `null` if there are no parameters. | |
| final Map<String, dynamic>? params; | |
| /// The time at which the request was sent. | |
| final int requestTime; | |
| /// The time at which the response was received, or `null` if no response has | |
| /// been received. | |
| int? responseTime; | |
| /// The response that was received. | |
| Response? _response; | |
| /// The completer that will be completed when a response is received. | |
| Completer<Response>? _responseCompleter; | |
| /// Initialize a newly created set of request data. | |
| new(this.id, this.method, this.params, this.requestTime); | |
| /// Return the number of milliseconds that elapsed between the request and the | |
| /// response. This getter assumes that the response was received. | |
| int get elapsedTime => responseTime! - requestTime; | |
| /// Return a future that will complete when a response is received. | |
| Future<Response> get respondedTo { | |
| var response = _response; | |
| if (response != null) { | |
| return Future.value(response); | |
| } | |
| var completer = _responseCompleter ??= Completer<Response>(); | |
| return completer.future; | |
| } | |
| /// Record that the given [response] was received. | |
| void recordResponse(Response response) { | |
| if (_response != null) { | |
| stdout.writeln( | |
| 'Received a second response to a $method request (id = $id)', | |
| ); | |
| return; | |
| } | |
| responseTime = currentTime; | |
| _response = response; | |
| var completer = _responseCompleter; | |
| if (completer != null) { | |
| completer.complete(response); | |
| _responseCompleter = null; | |
| } | |
| } | |
| } | |
| /// A utility for starting and communicating with an analysis server that is | |
| /// running in a separate process. | |
| class Server { | |
| /// The label used for communications from the client. | |
| static const String fromClient = 'client'; | |
| /// The label used for normal communications from the server. | |
| static const String fromServer = 'server'; | |
| /// The label used for output written by the server on [fromStderr]. | |
| static const String fromStderr = 'stderr'; | |
| /// The logger to which the communications log should be written, or `null` if | |
| /// the log should not be written. | |
| final Logger? logger; | |
| /// The process in which the server is running, or `null` if the server hasn't | |
| /// been started yet. | |
| Process? _process; | |
| /// Number that should be used to compute the 'id' to send in the next command | |
| /// sent to the server. | |
| int _nextId = 0; | |
| /// The analysis roots that are included. | |
| List<String> _analysisRootIncludes = <String>[]; | |
| /// A list containing the paths of files for which an overlay has been | |
| /// created. | |
| List<String> filesWithOverlays = <String>[]; | |
| /// The files that the server reported as being analyzed. | |
| List<String> _analyzedFiles = <String>[]; | |
| /// A mapping from the absolute paths of files to the most recent set of | |
| /// errors received for that file. | |
| final ErrorMap _errorMap = ErrorMap(); | |
| /// The completer that will be completed the next time a 'server.status' | |
| /// notification is received from the server with 'analyzing' set to false. | |
| Completer<void>? _analysisFinishedCompleter; | |
| /// The completer that will be completed the next time a 'server.connected' | |
| /// notification is received from the server. | |
| Completer<void>? _serverConnectedCompleter; | |
| /// A table mapping the ids of requests that have been sent to the server to | |
| /// data about those requests. | |
| final Map<String, RequestData> _requestDataMap = <String, RequestData>{}; | |
| /// A table mapping the number of times a request whose 'event' is equal to | |
| /// the key was sent to the server. | |
| final Map<String, int> _notificationCountMap = <String, int>{}; | |
| /// Initialize a new analysis server. The analysis server is not running and | |
| /// must be started using [start]. | |
| /// | |
| /// If a [logger] is provided, the communications between the client (this | |
| /// test) and the server will be written to it. | |
| new({this.logger}); | |
| /// Return a future that will complete when a 'server.status' notification is | |
| /// received from the server with 'analyzing' set to false. | |
| /// | |
| /// The future will only be completed by 'server.status' notifications that | |
| /// are received after this function call, so it is safe to use this getter | |
| /// multiple times in one test; each time it is used it will wait afresh for | |
| /// analysis to finish. | |
| Future<void> get analysisFinished { | |
| var completer = _analysisFinishedCompleter ??= Completer<void>(); | |
| return completer.future; | |
| } | |
| /// Return a list of the paths of files that are currently being analyzed. | |
| List<String> get analyzedDartFiles { | |
| bool isAnalyzed(String filePath) { | |
| // TODO(brianwilkerson): This should use the path package to determine | |
| // inclusion, and needs to take exclusions into account. | |
| for (var includedRoot in _analysisRootIncludes) { | |
| if (filePath.startsWith(includedRoot)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| var analyzedFiles = <String>[]; | |
| for (var filePath in _analyzedFiles) { | |
| if (filePath.endsWith('.dart') && isAnalyzed(filePath)) { | |
| analyzedFiles.add(filePath); | |
| } | |
| } | |
| return analyzedFiles; | |
| } | |
| /// Return a table mapping the absolute paths of files to the most recent set | |
| /// of errors received for that file. The content of the map will not change | |
| /// when new sets of errors are received. | |
| ErrorMap get errorMap => ErrorMap.from(_errorMap); | |
| /// Compute a mapping from each of the file paths in the given list of | |
| /// [filePaths] to the list of errors in the file at that path. | |
| Future<ErrorMap> computeErrorMap(List<String> filePaths) async { | |
| var errorMap = ErrorMap(); | |
| var futures = <Future<void>>[]; | |
| for (var filePath in filePaths) { | |
| var requestData = sendAnalysisGetErrors(filePath); | |
| futures.add( | |
| requestData.respondedTo.then((Response response) { | |
| if (response.result != null) { | |
| var result = AnalysisGetErrorsResult.fromResponse( | |
| response, | |
| clientUriConverter: null, | |
| ); | |
| errorMap[filePath] = result.errors; | |
| } | |
| }), | |
| ); | |
| } | |
| await Future.wait(futures); | |
| return errorMap; | |
| } | |
| /// Print information about the communications with the server. | |
| void printStatistics() { | |
| void writeSpaces(int count) { | |
| for (var i = 0; i < count; i++) { | |
| stdout.write(' '); | |
| } | |
| } | |
| // | |
| // Print information about the requests that were sent. | |
| // | |
| stdout.writeln('Request Counts'); | |
| if (_requestDataMap.isEmpty) { | |
| stdout.writeln(' none'); | |
| } else { | |
| var requestsByMethod = <String, List<RequestData>>{}; | |
| for (var requestData in _requestDataMap.values) { | |
| requestsByMethod | |
| .putIfAbsent(requestData.method, () => <RequestData>[]) | |
| .add(requestData); | |
| } | |
| var keys = requestsByMethod.keys.toList(); | |
| keys.sort(); | |
| var maxCount = requestsByMethod.values.fold( | |
| 0, | |
| (int count, List<RequestData> list) => count + list.length, | |
| ); | |
| var countWidth = maxCount.toString().length; | |
| for (var key in keys) { | |
| var requests = requestsByMethod[key]!; | |
| var noResponseCount = 0; | |
| var responseCount = 0; | |
| var minTime = -1; | |
| var maxTime = -1; | |
| var totalTime = 0; | |
| for (var data in requests) { | |
| if (data.responseTime == null) { | |
| noResponseCount++; | |
| } else { | |
| responseCount++; | |
| var time = data.elapsedTime; | |
| minTime = minTime < 0 ? time : math.min(minTime, time); | |
| maxTime = math.max(maxTime, time); | |
| totalTime += time; | |
| } | |
| } | |
| var count = requests.length.toString(); | |
| writeSpaces(countWidth - count.length); | |
| stdout.write(' '); | |
| stdout.write(count); | |
| stdout.write(' - '); | |
| stdout.write(key); | |
| if (noResponseCount > 0) { | |
| stdout.write(', '); | |
| stdout.write(noResponseCount); | |
| stdout.write(' with no response'); | |
| } | |
| if (maxTime >= 0) { | |
| stdout.write(' ('); | |
| stdout.write(minTime); | |
| stdout.write(', '); | |
| stdout.write(totalTime / responseCount); | |
| stdout.write(', '); | |
| stdout.write(maxTime); | |
| stdout.write(')'); | |
| } | |
| stdout.writeln(); | |
| } | |
| } | |
| // | |
| // Print information about the notifications that were received. | |
| // | |
| stdout.writeln(); | |
| stdout.writeln('Notification Counts'); | |
| if (_notificationCountMap.isEmpty) { | |
| stdout.writeln(' none'); | |
| } else { | |
| var keys = _notificationCountMap.keys.toList(); | |
| keys.sort(); | |
| var maxCount = _notificationCountMap.values.fold(0, math.max); | |
| var countWidth = maxCount.toString().length; | |
| for (var key in keys) { | |
| var count = _notificationCountMap[key].toString(); | |
| writeSpaces(countWidth - count.length); | |
| stdout.write(' '); | |
| stdout.write(count); | |
| stdout.write(' - '); | |
| stdout.writeln(key); | |
| } | |
| } | |
| } | |
| /// Remove any existing overlays. | |
| void removeAllOverlays() { | |
| var files = <String, Object>{}; | |
| for (var path in filesWithOverlays) { | |
| files[path] = RemoveContentOverlay(); | |
| } | |
| sendAnalysisUpdateContent(files); | |
| } | |
| RequestData sendAnalysisGetErrors(String file) { | |
| var params = AnalysisGetErrorsParams(file).toJson(clientUriConverter: null); | |
| return _send('analysis.getErrors', params); | |
| } | |
| RequestData sendAnalysisGetHover(String file, int offset) { | |
| var params = AnalysisGetHoverParams( | |
| file, | |
| offset, | |
| ).toJson(clientUriConverter: null); | |
| return _send('analysis.getHover', params); | |
| } | |
| RequestData sendAnalysisGetLibraryDependencies() { | |
| return _send('analysis.getLibraryDependencies', null); | |
| } | |
| RequestData sendAnalysisGetNavigation(String file, int offset, int length) { | |
| var params = AnalysisGetNavigationParams( | |
| file, | |
| offset, | |
| length, | |
| ).toJson(clientUriConverter: null); | |
| return _send('analysis.getNavigation', params); | |
| } | |
| RequestData sendAnalysisGetReachableSources(String file) { | |
| var params = AnalysisGetReachableSourcesParams(file) | |
| .toJson(clientUriConverter: null); | |
| return _send('analysis.getReachableSources', params); | |
| } | |
| void sendAnalysisReanalyze() { | |
| var params = AnalysisReanalyzeParams().toJson(clientUriConverter: null); | |
| _send('analysis.reanalyze', params); | |
| } | |
| void sendAnalysisSetAnalysisRoots( | |
| List<String> included, | |
| List<String> excluded, { | |
| Map<String, String>? packageRoots, | |
| }) { | |
| _analysisRootIncludes = included; | |
| var params = AnalysisSetAnalysisRootsParams( | |
| included, | |
| excluded, | |
| packageRoots: packageRoots, | |
| ).toJson(clientUriConverter: null); | |
| _send('analysis.setAnalysisRoots', params); | |
| } | |
| void sendAnalysisSetGeneralSubscriptions( | |
| List<GeneralAnalysisService> subscriptions, | |
| ) { | |
| var params = AnalysisSetGeneralSubscriptionsParams(subscriptions) | |
| .toJson(clientUriConverter: null); | |
| _send('analysis.setGeneralSubscriptions', params); | |
| } | |
| void sendAnalysisSetPriorityFiles(List<String> files) { | |
| var params = AnalysisSetPriorityFilesParams(files) | |
| .toJson(clientUriConverter: null); | |
| _send('analysis.setPriorityFiles', params); | |
| } | |
| void sendAnalysisSetSubscriptions( | |
| Map<AnalysisService, List<String>> subscriptions, | |
| ) { | |
| var params = AnalysisSetSubscriptionsParams(subscriptions) | |
| .toJson(clientUriConverter: null); | |
| _send('analysis.setSubscriptions', params); | |
| } | |
| void sendAnalysisUpdateContent(Map<String, Object> files) { | |
| files.forEach((path, overlay) { | |
| if (overlay is AddContentOverlay) { | |
| filesWithOverlays.add(path); | |
| } else if (overlay is RemoveContentOverlay) { | |
| filesWithOverlays.remove(path); | |
| } | |
| }); | |
| var params = AnalysisUpdateContentParams(files) | |
| .toJson(clientUriConverter: null); | |
| _send('analysis.updateContent', params); | |
| } | |
| void sendAnalysisUpdateOptions(AnalysisOptions options) { | |
| var params = AnalysisUpdateOptionsParams(options) | |
| .toJson(clientUriConverter: null); | |
| _send('analysis.updateOptions', params); | |
| } | |
| RequestData sendDiagnosticGetDiagnostics() { | |
| return _send('diagnostic.getDiagnostics', null); | |
| } | |
| RequestData sendEditFormat( | |
| String file, | |
| int selectionOffset, | |
| int selectionLength, { | |
| int? lineLength, | |
| }) { | |
| var params = EditFormatParams( | |
| file, | |
| selectionOffset, | |
| selectionLength, | |
| lineLength: lineLength, | |
| ).toJson(clientUriConverter: null); | |
| return _send('edit.format', params); | |
| } | |
| RequestData sendEditGetAssists(String file, int offset, int length) { | |
| var params = EditGetAssistsParams( | |
| file, | |
| offset, | |
| length, | |
| ).toJson(clientUriConverter: null); | |
| return _send('edit.getAssists', params); | |
| } | |
| RequestData sendEditGetAvailableRefactorings( | |
| String file, | |
| int offset, | |
| int length, | |
| ) { | |
| var params = EditGetAvailableRefactoringsParams( | |
| file, | |
| offset, | |
| length, | |
| ).toJson(clientUriConverter: null); | |
| return _send('edit.getAvailableRefactorings', params); | |
| } | |
| RequestData sendEditGetFixes(String file, int offset) { | |
| var params = EditGetFixesParams( | |
| file, | |
| offset, | |
| ).toJson(clientUriConverter: null); | |
| return _send('edit.getFixes', params); | |
| } | |
| RequestData sendEditGetRefactoring( | |
| RefactoringKind kind, | |
| String file, | |
| int offset, | |
| int length, | |
| bool validateOnly, { | |
| RefactoringOptions? options, | |
| }) { | |
| var params = EditGetRefactoringParams( | |
| kind, | |
| file, | |
| offset, | |
| length, | |
| validateOnly, | |
| options: options, | |
| ).toJson(clientUriConverter: null); | |
| return _send('edit.getRefactoring', params); | |
| } | |
| RequestData sendEditOrganizeDirectives(String file) { | |
| var params = EditOrganizeDirectivesParams(file) | |
| .toJson(clientUriConverter: null); | |
| return _send('edit.organizeDirectives', params); | |
| } | |
| RequestData sendEditSortMembers(String file) { | |
| var params = EditSortMembersParams(file).toJson(clientUriConverter: null); | |
| return _send('edit.sortMembers', params); | |
| } | |
| RequestData sendExecutionCreateContext(String contextRoot) { | |
| var params = ExecutionCreateContextParams(contextRoot) | |
| .toJson(clientUriConverter: null); | |
| return _send('execution.createContext', params); | |
| } | |
| RequestData sendExecutionDeleteContext(String id) { | |
| var params = ExecutionDeleteContextParams(id) | |
| .toJson(clientUriConverter: null); | |
| return _send('execution.deleteContext', params); | |
| } | |
| RequestData sendExecutionMapUri(String id, {String? file, String? uri}) { | |
| var params = ExecutionMapUriParams( | |
| id, | |
| file: file, | |
| uri: uri, | |
| ).toJson(clientUriConverter: null); | |
| return _send('execution.mapUri', params); | |
| } | |
| RequestData sendExecutionSetSubscriptions( | |
| List<ExecutionService> subscriptions, | |
| ) { | |
| var params = ExecutionSetSubscriptionsParams(subscriptions) | |
| .toJson(clientUriConverter: null); | |
| return _send('execution.setSubscriptions', params); | |
| } | |
| void sendSearchFindElementReferences( | |
| String file, | |
| int offset, | |
| bool includePotential, | |
| ) { | |
| var params = SearchFindElementReferencesParams( | |
| file, | |
| offset, | |
| includePotential, | |
| ).toJson(clientUriConverter: null); | |
| _send('search.findElementReferences', params); | |
| } | |
| void sendSearchFindMemberDeclarations(String name) { | |
| var params = SearchFindMemberDeclarationsParams(name) | |
| .toJson(clientUriConverter: null); | |
| _send('search.findMemberDeclarations', params); | |
| } | |
| void sendSearchFindMemberReferences(String name) { | |
| var params = SearchFindMemberReferencesParams(name) | |
| .toJson(clientUriConverter: null); | |
| _send('search.findMemberReferences', params); | |
| } | |
| void sendSearchFindTopLevelDeclarations(String pattern) { | |
| var params = SearchFindTopLevelDeclarationsParams(pattern) | |
| .toJson(clientUriConverter: null); | |
| _send('search.findTopLevelDeclarations', params); | |
| } | |
| void sendSearchGetTypeHierarchy(String file, int offset, {bool? superOnly}) { | |
| var params = SearchGetTypeHierarchyParams( | |
| file, | |
| offset, | |
| superOnly: superOnly, | |
| ).toJson(clientUriConverter: null); | |
| _send('search.getTypeHierarchy', params); | |
| } | |
| RequestData sendServerGetVersion() { | |
| return _send('server.getVersion', null); | |
| } | |
| void sendServerSetSubscriptions(List<ServerService> subscriptions) { | |
| var params = ServerSetSubscriptionsParams(subscriptions) | |
| .toJson(clientUriConverter: null); | |
| _send('server.setSubscriptions', params); | |
| } | |
| void sendServerShutdown() { | |
| _send('server.shutdown', null); | |
| } | |
| /// Start the server and listen for communications from it. | |
| /// | |
| /// If [checked] is `true`, the server's VM will be running in checked mode. | |
| /// | |
| /// If [diagnosticPort] is not `null`, the server will serve status pages to | |
| /// the specified port. | |
| /// | |
| /// If [profileServer] is `true`, the server will be started with "--observe" | |
| /// and "--pause-isolates-on-exit", allowing Dart DevTools to be used. | |
| /// | |
| /// If [useAnalysisHighlight2] is `true`, the server will use the new | |
| /// highlight APIs. | |
| Future<void> start({ | |
| bool checked = true, | |
| int? diagnosticPort, | |
| bool profileServer = false, | |
| String? sdkPath, | |
| int? servicePort, | |
| bool useAnalysisHighlight2 = false, | |
| }) async { | |
| if (_process != null) { | |
| throw Exception('Process already started'); | |
| } | |
| var dartBinary = Platform.executable; | |
| var rootDir = _findRoot( | |
| Platform.script.toFilePath(windows: Platform.isWindows), | |
| ); | |
| var serverPath = path.normalize(path.join(rootDir, 'bin', 'server.dart')); | |
| var arguments = <String>[]; | |
| // | |
| // Add VM arguments. | |
| // | |
| if (profileServer) { | |
| if (servicePort == null) { | |
| arguments.add('--observe'); | |
| } else { | |
| arguments.add('--observe=$servicePort'); | |
| } | |
| arguments.add('--pause-isolates-on-exit'); | |
| } else if (servicePort != null) { | |
| arguments.add('--enable-vm-service=$servicePort'); | |
| } | |
| if (Platform.packageConfig != null) { | |
| arguments.add('--packages=${Platform.packageConfig}'); | |
| } | |
| if (checked) { | |
| arguments.add('--checked'); | |
| } | |
| // | |
| // Add the server executable. | |
| // | |
| arguments.add(serverPath); | |
| // | |
| // Add server arguments. | |
| // | |
| if (diagnosticPort != null) { | |
| arguments.add('--port'); | |
| arguments.add(diagnosticPort.toString()); | |
| } | |
| if (sdkPath != null) { | |
| arguments.add('--sdk=$sdkPath'); | |
| } | |
| if (useAnalysisHighlight2) { | |
| arguments.add('--useAnalysisHighlight2'); | |
| } | |
| // stdout.writeln('Launching $serverPath'); | |
| // stdout.writeln('$dartBinary ${arguments.join(' ')}'); | |
| _process = await Process.start(dartBinary, arguments); | |
| unawaited( | |
| _process!.exitCode.then((int code) { | |
| if (code != 0) { | |
| throw StateError('Server terminated with exit code $code'); | |
| } | |
| }), | |
| ); | |
| _listenToOutput(); | |
| var completer = Completer<void>(); | |
| _serverConnectedCompleter = completer; | |
| return completer.future; | |
| } | |
| /// Find the root directory of the analysis_server package by proceeding | |
| /// upward to the 'test' dir, and then going up one more directory. | |
| String _findRoot(String pathname) { | |
| while (!['benchmark', 'test'].contains(path.basename(pathname))) { | |
| var parent = path.dirname(pathname); | |
| if (parent.length >= pathname.length) { | |
| throw Exception("Can't find root directory"); | |
| } | |
| pathname = parent; | |
| } | |
| return path.dirname(pathname); | |
| } | |
| /// Handle a [notification] received from the server. | |
| void _handleNotification(Notification notification) { | |
| switch (notification.event) { | |
| case 'server.connected': | |
| // new ServerConnectedParams.fromNotification(notification, clientUriConverter: null); | |
| _serverConnectedCompleter!.complete(); | |
| case 'server.error': | |
| // new ServerErrorParams.fromNotification(notification, clientUriConverter: null); | |
| throw StateError('Server error: ${notification.toJson()}'); | |
| case 'server.status': | |
| if (_analysisFinishedCompleter != null) { | |
| var params = ServerStatusParams.fromNotification( | |
| notification, | |
| clientUriConverter: null, | |
| ); | |
| var analysis = params.analysis; | |
| if (analysis != null && !analysis.isAnalyzing) { | |
| _analysisFinishedCompleter!.complete(); | |
| } | |
| } | |
| case 'analysis.analyzedFiles': | |
| var params = AnalysisAnalyzedFilesParams.fromNotification( | |
| notification, | |
| clientUriConverter: null, | |
| ); | |
| _analyzedFiles = params.directories; | |
| case 'analysis.errors': | |
| var params = AnalysisErrorsParams.fromNotification( | |
| notification, | |
| clientUriConverter: null, | |
| ); | |
| _errorMap.pathMap[params.file] = params.errors; | |
| case 'analysis.flushResults': | |
| // new AnalysisFlushResultsParams.fromNotification(notification, clientUriConverter: null); | |
| _errorMap.pathMap.clear(); | |
| case 'analysis.folding': | |
| // new AnalysisFoldingParams.fromNotification(notification, clientUriConverter: null); | |
| break; | |
| case 'analysis.highlights': | |
| // new AnalysisHighlightsParams.fromNotification(notification, clientUriConverter: null); | |
| break; | |
| case 'analysis.implemented': | |
| // new AnalysisImplementedParams.fromNotification(notification, clientUriConverter: null); | |
| break; | |
| case 'analysis.invalidate': | |
| // new AnalysisInvalidateParams.fromNotification(notification, clientUriConverter: null); | |
| break; | |
| case 'analysis.navigation': | |
| // new AnalysisNavigationParams.fromNotification(notification, clientUriConverter: null); | |
| break; | |
| case 'analysis.occurrences': | |
| // new AnalysisOccurrencesParams.fromNotification(notification, clientUriConverter: null); | |
| break; | |
| case 'analysis.outline': | |
| // new AnalysisOutlineParams.fromNotification(notification, clientUriConverter: null); | |
| break; | |
| case 'analysis.overrides': | |
| // new AnalysisOverridesParams.fromNotification(notification, clientUriConverter: null); | |
| break; | |
| case 'completion.results': | |
| // new CompletionResultsParams.fromNotification(notification, clientUriConverter: null); | |
| break; | |
| case 'search.results': | |
| // new SearchResultsParams.fromNotification(notification, clientUriConverter: null); | |
| break; | |
| case 'execution.launchData': | |
| // new ExecutionLaunchDataParams.fromNotification(notification, clientUriConverter: null); | |
| break; | |
| default: | |
| throw StateError('Unhandled notification: ${notification.toJson()}'); | |
| } | |
| } | |
| /// Handle a [response] received from the server. | |
| void _handleResponse(Response response) { | |
| var id = response.id.toString(); | |
| var requestData = _requestDataMap[id]!; | |
| requestData.recordResponse(response); | |
| // switch (requestData.method) { | |
| // case "analysis.getErrors": | |
| // break; | |
| // case "analysis.getHover": | |
| // break; | |
| // case "analysis.getLibraryDependencies": | |
| // break; | |
| // case "analysis.getNavigation": | |
| // break; | |
| // case "analysis.getReachableSources": | |
| // break; | |
| // case "analysis.reanalyze": | |
| // break; | |
| // case "analysis.setAnalysisRoots": | |
| // break; | |
| // case "analysis.setGeneralSubscriptions": | |
| // break; | |
| // case "analysis.setPriorityFiles": | |
| // break; | |
| // case "analysis.setSubscriptions": | |
| // break; | |
| // case 'analysis.updateContent': | |
| // break; | |
| // case "analysis.updateOptions": | |
| // break; | |
| // case "completion.getSuggestions": | |
| // break; | |
| // case "diagnostic.getDiagnostics": | |
| // break; | |
| // case "edit.format": | |
| // break; | |
| // case "edit.getAssists": | |
| // break; | |
| // case "edit.getAvailableRefactorings": | |
| // break; | |
| // case "edit.getFixes": | |
| // break; | |
| // case "edit.getRefactoring": | |
| // break; | |
| // case "edit.organizeDirectives": | |
| // break; | |
| // case "edit.sortMembers": | |
| // break; | |
| // case "execution.createContext": | |
| // break; | |
| // case "execution.deleteContext": | |
| // break; | |
| // case "execution.mapUri": | |
| // break; | |
| // case "execution.setSubscriptions": | |
| // break; | |
| // case "search.findElementReferences": | |
| // break; | |
| // case "search.findMemberDeclarations": | |
| // break; | |
| // case "search.findMemberReferences": | |
| // break; | |
| // case "search.findTopLevelDeclarations": | |
| // break; | |
| // case "search.getTypeHierarchy": | |
| // break; | |
| // case "server.getVersion": | |
| // break; | |
| // case "server.setSubscriptions": | |
| // break; | |
| // case "server.shutdown": | |
| // break; | |
| // default: | |
| // throw new StateError('Unhandled response: ${response.toJson(clientUriConverter: null)}'); | |
| // } | |
| } | |
| /// Handle a [line] of input read from stderr. | |
| void _handleStdErr(String line) { | |
| var trimmedLine = line.trim(); | |
| logger?.log(fromStderr, trimmedLine); | |
| throw StateError('Message received on stderr: "$trimmedLine"'); | |
| } | |
| /// Handle a [line] of input read from stdout. | |
| void _handleStdOut(String line) { | |
| /// Cast the given [value] to a Map, or throw an [ArgumentError] if the | |
| /// value cannot be cast. | |
| Map<String, Object?> asMap(Object value) { | |
| if (value is Map<String, Object?>) { | |
| return value; | |
| } | |
| throw ArgumentError('Expected a Map, found a ${value.runtimeType}'); | |
| } | |
| var trimmedLine = line.trim(); | |
| if (trimmedLine.isEmpty || | |
| trimmedLine.startsWith('The Dart VM service is listening on ')) { | |
| return; | |
| } | |
| logger?.log(fromServer, trimmedLine); | |
| var message = asMap(json.decoder.convert(trimmedLine) as Object); | |
| if (message.containsKey('id')) { | |
| // The message is a response. | |
| var response = Response.fromJson(message)!; | |
| _handleResponse(response); | |
| } else { | |
| // The message is a notification. | |
| var notification = Notification.fromJson(message); | |
| var event = notification.event; | |
| _notificationCountMap[event] = (_notificationCountMap[event] ?? 0) + 1; | |
| _handleNotification(notification); | |
| } | |
| } | |
| /// Start listening to output from the server. | |
| void _listenToOutput() { | |
| /// Install the given [handler] to listen to transformed output from the | |
| /// given [stream]. | |
| void installHandler( | |
| Stream<List<int>> stream, | |
| void Function(String) handler, | |
| ) { | |
| stream | |
| .transform(Utf8Codec().decoder) | |
| .transform(LineSplitter()) | |
| .listen(handler); | |
| } | |
| installHandler(_process!.stdout, _handleStdOut); | |
| installHandler(_process!.stderr, _handleStdErr); | |
| } | |
| /// Send a command to the server. An 'id' will be automatically assigned. | |
| RequestData _send(String method, Map<String, dynamic>? params) { | |
| var id = '${_nextId++}'; | |
| var requestData = RequestData(id, method, params, currentTime); | |
| _requestDataMap[id] = requestData; | |
| var command = <String, dynamic>{'id': id, 'method': method}; | |
| if (params != null) { | |
| command['params'] = params; | |
| } | |
| var line = json.encode(command); | |
| _process!.stdin.add(utf8.encode('$line\n')); | |
| logger?.log(fromClient, line); | |
| return requestData; | |
| } | |
| } |