blob: e237c6b3c236b559b78577ba91f2d54e79184367 [file] [log] [blame]
// Copyright (c) 2022, 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/src/handler/legacy/completion_utils.dart';
import 'package:analysis_server/src/handler/legacy/legacy_handler.dart';
import 'package:analysis_server/src/legacy_analysis_server.dart';
import 'package:analysis_server/src/protocol_server.dart';
import 'package:analysis_server/src/provisional/completion/completion_core.dart';
import 'package:analysis_server/src/request_handler_mixin.dart';
import 'package:analysis_server/src/services/completion/completion_performance.dart';
import 'package:analysis_server/src/services/completion/dart/candidate_suggestion.dart';
import 'package:analysis_server/src/services/completion/dart/completion_manager.dart';
import 'package:analysis_server/src/services/completion/dart/suggestion_builder.dart';
import 'package:analysis_server/src/services/completion/yaml/analysis_options_generator.dart';
import 'package:analysis_server/src/services/completion/yaml/fix_data_generator.dart';
import 'package:analysis_server/src/services/completion/yaml/pubspec_generator.dart';
import 'package:analysis_server/src/services/completion/yaml/yaml_completion_generator.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:analyzer/src/util/performance/operation_performance.dart';
/// The handler for the `completion.getSuggestions2` request.
class CompletionGetSuggestions2Handler extends CompletionHandler
with RequestHandlerMixin<LegacyAnalysisServer> {
/// Initialize a newly created handler to be able to service requests for the
/// [server].
CompletionGetSuggestions2Handler(
super.server,
super.request,
super.cancellationToken,
super.performance,
);
/// Computes completion results for [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<List<CandidateSuggestion>> computeFinalSuggestions({
required CompletionBudget budget,
required OperationPerformanceImpl performance,
required DartCompletionRequest request,
required int maxSuggestions,
NotImportedSuggestions? notImportedSuggestions,
required bool useFilter,
required DartCompletionManager manager,
}) async {
//
// Compute completions generated by server.
//
return await performance.runAsync('computeSuggestions', (
performance,
) async {
var collector = await manager.computeFinalizedCandidateSuggestions(
request: request,
performance: performance,
maxSuggestions: maxSuggestions,
);
return collector.suggestions;
});
}
/// Computes completion results for [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<List<CompletionSuggestionBuilder>> computeSuggestions({
required CompletionBudget budget,
required OperationPerformanceImpl performance,
required DartCompletionRequest request,
required int maxSuggestions,
NotImportedSuggestions? notImportedSuggestions,
required bool useFilter,
}) async {
//
// Compute completions generated by server.
//
var suggestions = <CompletionSuggestionBuilder>[];
await performance.runAsync('computeSuggestions', (performance) async {
var manager = DartCompletionManager(
budget: budget,
notImportedSuggestions: notImportedSuggestions,
);
suggestions.addAll(
await manager.computeSuggestions(
request,
performance,
maxSuggestions: maxSuggestions,
useFilter: useFilter,
),
);
});
// TODO(danrubel): if request is obsolete (processAnalysisRequest returns
// false) then send empty results
return suggestions;
}
/// Return the suggestions that should be presented in the YAML [file] at the
/// given [offset].
YamlCompletionResults computeYamlSuggestions(String file, int offset) {
var provider = server.resourceProvider;
var pathContext = provider.pathContext;
if (file_paths.isAnalysisOptionsYaml(pathContext, file)) {
var generator = AnalysisOptionsGenerator(provider);
return generator.getSuggestions(file, offset);
} else if (file_paths.isFixDataYaml(pathContext, file)) {
var generator = FixDataGenerator(provider);
return generator.getSuggestions(file, offset);
} else if (file_paths.isPubspecYaml(pathContext, file)) {
var generator = PubspecGenerator(provider, server.pubPackageService);
return generator.getSuggestions(file, offset);
}
return const YamlCompletionResults.empty();
}
@override
Future<void> handle() async {
if (completionIsDisabled) {
return;
}
var requestLatency = request.timeSinceRequest;
var params = CompletionGetSuggestions2Params.fromRequest(
request,
clientUriConverter: server.uriConverter,
);
var file = params.file;
var offset = params.offset;
var timeoutMilliseconds = params.timeout;
var budget = CompletionBudget(
timeoutMilliseconds != null
? Duration(milliseconds: timeoutMilliseconds)
: server.completionState.budgetDuration,
);
var provider = server.resourceProvider;
var pathContext = provider.pathContext;
if (file.endsWith('.yaml')) {
var suggestions = computeYamlSuggestions(file, offset);
server.sendResponse(
CompletionGetSuggestions2Result(
suggestions.replacementOffset,
suggestions.replacementLength,
suggestions.suggestions,
false,
).toResponse(request.id, clientUriConverter: server.uriConverter),
);
return;
}
if (!file_paths.isDart(pathContext, file)) {
server.sendResponse(
CompletionGetSuggestions2Result(
offset,
0,
[],
false,
).toResponse(request.id, clientUriConverter: server.uriConverter),
);
return;
}
await performance.runAsync('request', (performance) async {
var resolvedUnit = await performance.runAsync('resolveForCompletion', (
performance,
) {
return server.resolveForCompletion(
path: file,
offset: offset,
performance: performance,
);
});
if (resolvedUnit == null) {
server.sendResponse(Response.fileNotAnalyzed(request, file));
return;
}
if (offset < 0 || offset > resolvedUnit.content.length) {
server.sendResponse(
Response.invalidParameter(
request,
'params.offset',
'Expected offset between 0 and source length inclusive,'
' but found $offset',
),
);
return;
}
var completionPerformance = CompletionPerformance(
performance: performance,
path: file,
requestLatency: requestLatency,
content: resolvedUnit.content,
offset: offset,
);
server.recentPerformance.completion.add(completionPerformance);
var analysisSession = resolvedUnit.analysisSession;
var enclosingNode = resolvedUnit.parsedUnit;
var completionRequest = DartCompletionRequest(
analysisSession: analysisSession,
fileState: resolvedUnit.fileState,
filePath: resolvedUnit.path,
fileContent: resolvedUnit.content,
libraryFragment: resolvedUnit.unitElement,
enclosingNode: enclosingNode,
offset: offset,
unit: resolvedUnit.parsedUnit,
dartdocDirectiveInfo: server.getDartdocDirectiveInfoForSession(
analysisSession,
),
);
setNewRequest(completionRequest);
var notImportedSuggestions = NotImportedSuggestions();
var candidateSuggestions = <CandidateSuggestion>[];
var suggestions = <CompletionSuggestion>[];
var manager = DartCompletionManager(
budget: budget,
notImportedSuggestions: notImportedSuggestions,
);
try {
candidateSuggestions = await computeFinalSuggestions(
manager: manager,
budget: budget,
performance: performance,
request: completionRequest,
maxSuggestions: params.maxResults,
notImportedSuggestions: notImportedSuggestions,
useFilter: true,
);
} on AbortCompletion {
return server.sendResponse(
CompletionGetSuggestions2Result(
completionRequest.replacementOffset,
completionRequest.replacementLength,
[],
true,
).toResponse(request.id, clientUriConverter: server.uriConverter),
);
}
// Keep track of whether the set of results was truncated (because
// budget was exhausted).
bool isIncomplete =
(manager.notImportedSuggestions?.isIncomplete ?? false) ||
manager.isTruncated;
completionPerformance.transmittedSuggestionCount =
candidateSuggestions.length;
await performance.run('mapSuggestions', (performance) async {
for (var candidate in candidateSuggestions) {
var item = await candidateToCompletionSuggestion(
candidate,
completionRequest,
);
if (item != null) {
suggestions.add(item);
}
}
});
performance.run('sendResponse', (_) {
sendResult(
CompletionGetSuggestions2Result(
completionRequest.replacementOffset,
completionRequest.replacementLength,
suggestions,
isIncomplete,
),
);
});
});
}
void setNewRequest(DartCompletionRequest request) {
_abortCurrentRequest();
server.completionState.currentRequest = request;
}
/// Abort the current completion request, if any.
void _abortCurrentRequest() {
var currentRequest = server.completionState.currentRequest;
if (currentRequest != null) {
currentRequest.abort();
server.completionState.currentRequest = null;
}
}
}