blob: 39515410f79541ed3b543a2bfa9fd74876e8c6bd [file] [log] [blame]
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:analysis_server/protocol/protocol.dart';
import 'package:analysis_server/protocol/protocol_constants.dart';
import 'package:analysis_server/protocol/protocol_generated.dart';
import 'package:analysis_server/src/analysis_server.dart';
import 'package:analysis_server/src/collections.dart';
import 'package:analysis_server/src/domain_abstract.dart';
import 'package:analysis_server/src/domains/completion/available_suggestions.dart';
import 'package:analysis_server/src/plugin/plugin_manager.dart';
import 'package:analysis_server/src/provisional/completion/completion_core.dart';
import 'package:analysis_server/src/services/completion/completion_core.dart';
import 'package:analysis_server/src/services/completion/completion_performance.dart';
import 'package:analysis_server/src/services/completion/dart/completion_manager.dart';
import 'package:analysis_server/src/services/completion/token_details/token_detail_builder.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/session.dart';
import 'package:analyzer/src/dart/analysis/driver.dart';
import 'package:analyzer_plugin/protocol/protocol.dart' as plugin;
import 'package:analyzer_plugin/protocol/protocol_common.dart';
import 'package:analyzer_plugin/protocol/protocol_constants.dart' as plugin;
import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
import 'package:analyzer_plugin/utilities/change_builder/change_builder_dart.dart';
/**
* Instances of the class [CompletionDomainHandler] implement a [RequestHandler]
* that handles requests in the completion domain.
*/
class CompletionDomainHandler extends AbstractRequestHandler {
/**
* The maximum number of performance measurements to keep.
*/
static const int performanceListMaxLength = 50;
/**
* The completion services that the client is currently subscribed.
*/
final Set<CompletionService> subscriptions = Set<CompletionService>();
/**
* The next completion response id.
*/
int _nextCompletionId = 0;
/**
* Code completion performance for the last completion operation.
*/
CompletionPerformance performance;
/**
* A list of code completion performance measurements for the latest
* completion operation up to [performanceListMaxLength] measurements.
*/
final RecentBuffer<CompletionPerformance> performanceList =
new RecentBuffer<CompletionPerformance>(performanceListMaxLength);
/**
* The current request being processed or `null` if none.
*/
CompletionRequestImpl _currentRequest;
/**
* The identifiers of the latest `getSuggestionDetails` request.
* We use it to abort previous requests.
*/
int _latestGetSuggestionDetailsId = 0;
/**
* Initialize a new request handler for the given [server].
*/
CompletionDomainHandler(AnalysisServer server) : super(server);
/**
* Compute completion results for the given request and append them to the stream.
* Clients should not call this method directly as it is automatically called
* when a client listens to the stream returned by [results].
* Subclasses should override this method, append at least one result
* to the [controller], and close the controller stream once complete.
*/
Future<CompletionResult> computeSuggestions(
CompletionRequestImpl request,
CompletionGetSuggestionsParams params,
Set<ElementKind> includedElementKinds,
List<IncludedSuggestionRelevanceTag> includedSuggestionRelevanceTags,
) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
//
// Allow plugins to start computing fixes.
//
Map<PluginInfo, Future<plugin.Response>> pluginFutures;
plugin.CompletionGetSuggestionsParams requestParams;
String file = params.file;
int offset = params.offset;
AnalysisDriver driver = server.getAnalysisDriver(file);
if (driver != null) {
requestParams = new plugin.CompletionGetSuggestionsParams(file, offset);
pluginFutures = server.pluginManager
.broadcastRequest(requestParams, contextRoot: driver.contextRoot);
}
//
// Compute completions generated by server.
//
List<CompletionSuggestion> suggestions = <CompletionSuggestion>[];
if (request.result != null) {
const COMPUTE_SUGGESTIONS_TAG = 'computeSuggestions';
performance.logStartTime(COMPUTE_SUGGESTIONS_TAG);
var manager = new DartCompletionManager(
includedElementKinds: includedElementKinds,
includedSuggestionRelevanceTags: includedSuggestionRelevanceTags,
);
String contributorTag = 'computeSuggestions - ${manager.runtimeType}';
performance.logStartTime(contributorTag);
try {
suggestions.addAll(await manager.computeSuggestions(request));
} on AbortCompletion {
suggestions.clear();
}
performance.logElapseTime(contributorTag);
performance.logElapseTime(COMPUTE_SUGGESTIONS_TAG);
}
// TODO (danrubel) if request is obsolete (processAnalysisRequest returns
// false) then send empty results
//
// Add the fixes produced by plugins to the server-generated fixes.
//
if (pluginFutures != null) {
List<plugin.Response> responses = await waitForResponses(pluginFutures,
requestParameters: requestParams);
for (plugin.Response response in responses) {
plugin.CompletionGetSuggestionsResult result =
new plugin.CompletionGetSuggestionsResult.fromResponse(response);
if (result.results != null && result.results.isNotEmpty) {
if (suggestions.isEmpty) {
request.replacementOffset = result.replacementOffset;
request.replacementLength = result.replacementLength;
} else if (request.replacementOffset != result.replacementOffset &&
request.replacementLength != result.replacementLength) {
server.instrumentationService
.logError('Plugin completion-results dropped due to conflicting'
' replacement offset/length: ${result.toJson()}');
continue;
}
suggestions.addAll(result.results);
}
}
}
//
// Return the result.
//
return new CompletionResult(
request.replacementOffset, request.replacementLength, suggestions);
}
/**
* Process a `completion.getSuggestionDetails` request.
*/
void getSuggestionDetails(Request request) async {
var params = CompletionGetSuggestionDetailsParams.fromRequest(request);
var file = params.file;
if (server.sendResponseErrorIfInvalidFilePath(request, file)) {
return;
}
var libraryId = params.id;
var library = server.declarationsTracker.getLibrary(libraryId);
if (library == null) {
server.sendResponse(Response.invalidParameter(
request,
'libraryId',
'No such library: $libraryId',
));
return;
}
// The label might be `MyEnum.myValue`, but we import only `MyEnum`.
var requestedName = params.label;
if (requestedName.contains('.')) {
requestedName = requestedName.substring(
0,
requestedName.indexOf('.'),
);
}
const timeout = Duration(milliseconds: 1000);
var timer = Stopwatch()..start();
var id = ++_latestGetSuggestionDetailsId;
while (id == _latestGetSuggestionDetailsId && timer.elapsed < timeout) {
try {
var analysisDriver = server.getAnalysisDriver(file);
var session = analysisDriver.currentSession;
var fileElement = await session.getUnitElement(file);
var libraryPath = fileElement.element.librarySource.fullName;
var resolvedLibrary = await session.getResolvedLibrary(libraryPath);
var requestedLibraryElement = await session.getLibraryByUri(
library.uriStr,
);
var requestedElement =
requestedLibraryElement.exportNamespace.get(requestedName);
if (requestedElement == null) {
server.sendResponse(Response.invalidParameter(
request,
'label',
'No such element: $requestedName',
));
return;
}
var completion = params.label;
var builder = DartChangeBuilder(session);
await builder.addFileEdit(libraryPath, (builder) {
var result = builder.importLibraryElement(
targetLibrary: resolvedLibrary,
targetPath: libraryPath,
targetOffset: params.offset,
requestedLibrary: requestedLibraryElement,
requestedElement: requestedElement,
);
if (result.prefix != null) {
completion = '${result.prefix}.$completion';
}
});
server.sendResponse(
CompletionGetSuggestionDetailsResult(
completion,
change: builder.sourceChange,
).toResponse(request.id),
);
return;
} on InconsistentAnalysisException {
// Loop around to try again.
}
}
// Timeout or abort, send the empty response.
server.sendResponse(
CompletionGetSuggestionDetailsResult('').toResponse(request.id),
);
}
@override
Response handleRequest(Request request) {
return runZoned(() {
String requestName = request.method;
if (requestName == COMPLETION_REQUEST_GET_SUGGESTION_DETAILS) {
getSuggestionDetails(request);
return Response.DELAYED_RESPONSE;
} else if (requestName == COMPLETION_REQUEST_GET_SUGGESTIONS) {
processRequest(request);
return Response.DELAYED_RESPONSE;
} else if (requestName == COMPLETION_REQUEST_LIST_TOKEN_DETAILS) {
listTokenDetails(request);
return Response.DELAYED_RESPONSE;
} else if (requestName == COMPLETION_REQUEST_SET_SUBSCRIPTIONS) {
return setSubscriptions(request);
}
return null;
}, onError: (exception, stackTrace) {
server.sendServerErrorNotification(
'Failed to handle completion domain request: ${request.toJson()}',
exception,
stackTrace);
});
}
void ifMatchesRequestClear(CompletionRequest completionRequest) {
if (_currentRequest == completionRequest) {
_currentRequest = null;
}
}
/**
* Process a `completion.listTokenDetails` request.
*/
Future<void> listTokenDetails(Request request) async {
CompletionListTokenDetailsParams params =
CompletionListTokenDetailsParams.fromRequest(request);
String file = params.file;
if (server.sendResponseErrorIfInvalidFilePath(request, file)) {
return;
}
AnalysisDriver analysisDriver = server.getAnalysisDriver(file);
if (analysisDriver == null) {
server.sendResponse(Response.invalidParameter(
request,
'file',
'File is not being analyzed: $file',
));
}
AnalysisSession session = analysisDriver.currentSession;
ResolvedUnitResult result = await session.getResolvedUnit(file);
if (result.state != ResultState.VALID) {
server.sendResponse(Response.invalidParameter(
request,
'file',
'File does not exist or cannot be read: $file',
));
}
TokenDetailBuilder builder = new TokenDetailBuilder();
builder.visitNode(result.unit);
server.sendResponse(
CompletionListTokenDetailsResult(builder.details).toResponse(request.id),
);
}
/**
* Process a `completion.getSuggestions` request.
*/
Future<void> processRequest(Request request) async {
// TODO(brianwilkerson) Determine whether this await is necessary.
await null;
performance = new CompletionPerformance();
// extract and validate params
CompletionGetSuggestionsParams params =
new CompletionGetSuggestionsParams.fromRequest(request);
String file = params.file;
int offset = params.offset;
if (server.sendResponseErrorIfInvalidFilePath(request, file)) {
return;
}
ResolvedUnitResult resolvedUnit = await server.getResolvedUnit(file);
if (resolvedUnit?.state == ResultState.VALID) {
if (offset < 0 || offset > resolvedUnit.content.length) {
server.sendResponse(new Response.invalidParameter(
request,
'params.offset',
'Expected offset between 0 and source length inclusive,'
' but found $offset'));
return;
}
recordRequest(performance, file, resolvedUnit.content, offset);
}
CompletionRequestImpl completionRequest =
new CompletionRequestImpl(resolvedUnit, offset, performance);
String completionId = (_nextCompletionId++).toString();
setNewRequest(completionRequest);
// initial response without results
server.sendResponse(new CompletionGetSuggestionsResult(completionId)
.toResponse(request.id));
// If the client opted into using available suggestion sets,
// create the kinds set, so signal the completion manager about opt-in.
Set<ElementKind> includedElementKinds;
List<IncludedSuggestionRelevanceTag> includedSuggestionRelevanceTags;
if (subscriptions.contains(CompletionService.AVAILABLE_SUGGESTION_SETS)) {
includedElementKinds = Set<ElementKind>();
includedSuggestionRelevanceTags = <IncludedSuggestionRelevanceTag>[];
}
// Compute suggestions in the background
computeSuggestions(
completionRequest,
params,
includedElementKinds,
includedSuggestionRelevanceTags,
).then((CompletionResult result) {
List<IncludedSuggestionSet> includedSuggestionSets;
if (includedElementKinds != null && resolvedUnit != null) {
includedSuggestionSets = computeIncludedSetList(
server.declarationsTracker,
resolvedUnit,
);
} else {
includedSuggestionSets = [];
}
const SEND_NOTIFICATION_TAG = 'send notification';
performance.logStartTime(SEND_NOTIFICATION_TAG);
sendCompletionNotification(
completionId,
result.replacementOffset,
result.replacementLength,
result.suggestions,
includedSuggestionSets,
includedElementKinds?.toList(),
includedSuggestionRelevanceTags,
);
performance.logElapseTime(SEND_NOTIFICATION_TAG);
performance.notificationCount = 1;
performance.logFirstNotificationComplete('notification 1 complete');
performance.suggestionCountFirst = result.suggestions.length;
performance.suggestionCountLast = result.suggestions.length;
performance.complete();
}).whenComplete(() {
ifMatchesRequestClear(completionRequest);
});
}
/**
* If tracking code completion performance over time, then
* record addition information about the request in the performance record.
*/
void recordRequest(CompletionPerformance performance, String path,
String content, int offset) {
performance.path = path;
if (performanceListMaxLength == 0) {
return;
}
performance.setContentsAndOffset(content, offset);
performanceList.add(performance);
}
/**
* Send completion notification results.
*/
void sendCompletionNotification(
String completionId,
int replacementOffset,
int replacementLength,
Iterable<CompletionSuggestion> results,
List<IncludedSuggestionSet> includedSuggestionSets,
List<ElementKind> includedElementKinds,
List<IncludedSuggestionRelevanceTag> includedSuggestionRelevanceTags,
) {
server.sendNotification(
new CompletionResultsParams(
completionId,
replacementOffset,
replacementLength,
results,
true,
includedSuggestionSets: includedSuggestionSets,
includedElementKinds: includedElementKinds,
includedSuggestionRelevanceTags: includedSuggestionRelevanceTags,
).toNotification(),
);
}
void setNewRequest(CompletionRequest completionRequest) {
_abortCurrentRequest();
_currentRequest = completionRequest;
}
/**
* Implement the 'completion.setSubscriptions' request.
*/
Response setSubscriptions(Request request) {
var params = CompletionSetSubscriptionsParams.fromRequest(request);
subscriptions.clear();
subscriptions.addAll(params.subscriptions);
if (subscriptions.contains(CompletionService.AVAILABLE_SUGGESTION_SETS)) {
server.createDeclarationsTracker((change) {
server.sendNotification(
createCompletionAvailableSuggestionsNotification(change),
);
});
} else {
server.disposeDeclarationsTracker();
}
return CompletionSetSubscriptionsResult().toResponse(request.id);
}
/**
* Abort the current completion request, if any.
*/
void _abortCurrentRequest() {
if (_currentRequest != null) {
_currentRequest.abort();
_currentRequest = null;
}
}
}
/**
* The result of computing suggestions for code completion.
*/
class CompletionResult {
/**
* The length of the text to be replaced if the remainder of the identifier
* containing the cursor is to be replaced when the suggestion is applied
* (that is, the number of characters in the existing identifier).
*/
final int replacementLength;
/**
* The offset of the start of the text to be replaced. This will be different
* than the offset used to request the completion suggestions if there was a
* portion of an identifier before the original offset. In particular, the
* replacementOffset will be the offset of the beginning of said identifier.
*/
final int replacementOffset;
/**
* The suggested completions.
*/
final List<CompletionSuggestion> suggestions;
CompletionResult(
this.replacementOffset, this.replacementLength, this.suggestions);
}