blob: 6a4a0ff1461b28c2610f4bf4e52c1bf8ef031a45 [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/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: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;
/**
* 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 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);
/**
* Performance for the last priority change event.
*/
CompletionPerformance computeCachePerformance;
/**
* The current request being processed or `null` if none.
*/
CompletionRequestImpl _currentRequest;
/**
* 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) 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);
CompletionContributor contributor = new DartCompletionManager();
String contributorTag = 'computeSuggestions - ${contributor
.runtimeType}';
performance.logStartTime(contributorTag);
try {
suggestions.addAll(await contributor.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);
}
@override
Response handleRequest(Request request) {
return runZoned(() {
String requestName = request.method;
if (requestName == COMPLETION_REQUEST_GET_SUGGESTIONS) {
processRequest(request);
return Response.DELAYED_RESPONSE;
}
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.getSuggestions` request.
*/
Future<Null> 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 filePath = params.file;
int offset = params.offset;
AnalysisResult result = await server.getAnalysisResult(filePath);
if (result == null || !result.exists) {
if (server.onNoAnalysisCompletion != null) {
String completionId = (_nextCompletionId++).toString();
await server.onNoAnalysisCompletion(
request, this, params, performance, completionId);
return;
}
} else {
if (offset < 0 || offset > result.content.length) {
server.sendResponse(new Response.invalidParameter(
request,
'params.offset',
'Expected offset between 0 and source length inclusive,'
' but found $offset'));
return;
}
recordRequest(performance, filePath, result.content, offset);
}
CompletionRequestImpl completionRequest =
new CompletionRequestImpl(result, offset, performance);
String completionId = (_nextCompletionId++).toString();
setNewRequest(completionRequest);
// initial response without results
server.sendResponse(new CompletionGetSuggestionsResult(completionId)
.toResponse(request.id));
// Compute suggestions in the background
computeSuggestions(completionRequest, params)
.then((CompletionResult result) {
const SEND_NOTIFICATION_TAG = 'send notification';
performance.logStartTime(SEND_NOTIFICATION_TAG);
sendCompletionNotification(completionId, result.replacementOffset,
result.replacementLength, result.suggestions);
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) {
server.sendNotification(new CompletionResultsParams(
completionId, replacementOffset, replacementLength, results, true)
.toNotification());
}
void setNewRequest(CompletionRequest completionRequest) {
_abortCurrentRequest();
_currentRequest = completionRequest;
}
/**
* 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);
}