3cc518b3cc
This is a re-do of https://dart-review.googlesource.com/c/sdk/+/197880 that removes the support for Cider. We might try using this for Cider at some future date, but I didn't want that support to prevent us from getting this to users sooner. Change-Id: Ia5c06a8a3d7366770dedb4f51153c2f4004bb89e Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/201021 Reviewed-by: Konstantin Shcheglov <scheglov@google.com> Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
362 lines
12 KiB
Dart
362 lines
12 KiB
Dart
// Copyright (c) 2020, 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 'package:analysis_server/src/protocol_server.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/dart/local_library_contributor.dart';
|
|
import 'package:analysis_server/src/services/completion/dart/suggestion_builder.dart';
|
|
import 'package:analysis_server/src/services/completion/filtering/fuzzy_matcher.dart';
|
|
import 'package:analyzer/dart/analysis/results.dart';
|
|
import 'package:analyzer/dart/element/element.dart' show LibraryElement;
|
|
import 'package:analyzer/src/dart/analysis/performance_logger.dart';
|
|
import 'package:analyzer/src/dart/micro/resolve_file.dart';
|
|
import 'package:analyzer/src/dartdoc/dartdoc_directive_info.dart';
|
|
import 'package:analyzer/src/util/performance/operation_performance.dart';
|
|
import 'package:meta/meta.dart';
|
|
|
|
/// The cache that can be reuse for across multiple completion request.
|
|
///
|
|
/// It contains data that is relatively small, and does not include for
|
|
/// example types and elements.
|
|
class CiderCompletionCache {
|
|
final Map<String, _CiderImportedLibrarySuggestions> _importedLibraries = {};
|
|
}
|
|
|
|
class CiderCompletionComputer {
|
|
final PerformanceLog _logger;
|
|
final CiderCompletionCache _cache;
|
|
final FileResolver _fileResolver;
|
|
|
|
final OperationPerformanceImpl _performanceRoot =
|
|
OperationPerformanceImpl('<root>');
|
|
|
|
late DartCompletionRequestImpl _dartCompletionRequest;
|
|
|
|
/// Paths of imported libraries for which suggestions were (re)computed
|
|
/// during processing of this request. Does not include libraries that were
|
|
/// processed during previous requests, and reused from the cache now.
|
|
@visibleForTesting
|
|
final List<String> computedImportedLibraries = [];
|
|
|
|
CiderCompletionComputer(this._logger, this._cache, this._fileResolver);
|
|
|
|
/// Return completion suggestions for the file and position.
|
|
///
|
|
/// The [path] must be the absolute and normalized path of the file.
|
|
///
|
|
/// The content of the file has already been updated.
|
|
///
|
|
/// The [line] and [column] are zero based.
|
|
Future<CiderCompletionResult> compute({
|
|
required String path,
|
|
required int line,
|
|
required int column,
|
|
@visibleForTesting void Function(ResolvedUnitResult)? testResolvedUnit,
|
|
}) async {
|
|
return _performanceRoot.runAsync('completion', (performance) async {
|
|
var resolvedUnit = performance.run('resolution', (performance) {
|
|
return _fileResolver.resolve(
|
|
completionLine: line,
|
|
completionColumn: column,
|
|
path: path,
|
|
performance: performance,
|
|
);
|
|
});
|
|
|
|
if (testResolvedUnit != null) {
|
|
testResolvedUnit(resolvedUnit);
|
|
}
|
|
|
|
var lineInfo = resolvedUnit.lineInfo;
|
|
var offset = lineInfo.getOffsetOfLine(line) + column;
|
|
|
|
var completionRequest = CompletionRequestImpl(
|
|
resolvedUnit,
|
|
offset,
|
|
CompletionPerformance(),
|
|
);
|
|
var dartdocDirectiveInfo = DartdocDirectiveInfo();
|
|
|
|
var suggestions = await performance.runAsync(
|
|
'suggestions',
|
|
(performance) async {
|
|
var result = await _logger.runAsync('Compute suggestions', () async {
|
|
var includedElementKinds = <ElementKind>{};
|
|
var includedElementNames = <String>{};
|
|
var includedSuggestionRelevanceTags =
|
|
<IncludedSuggestionRelevanceTag>[];
|
|
|
|
var manager = DartCompletionManager(
|
|
dartdocDirectiveInfo: dartdocDirectiveInfo,
|
|
includedElementKinds: includedElementKinds,
|
|
includedElementNames: includedElementNames,
|
|
includedSuggestionRelevanceTags: includedSuggestionRelevanceTags,
|
|
);
|
|
|
|
return await manager.computeSuggestions(
|
|
performance,
|
|
completionRequest,
|
|
enableImportedReferenceContributor: false,
|
|
enableOverrideContributor: false,
|
|
enableUriContributor: false,
|
|
);
|
|
});
|
|
|
|
performance.getDataInt('count').add(result.length);
|
|
return result.toList();
|
|
},
|
|
);
|
|
|
|
_dartCompletionRequest = await DartCompletionRequestImpl.from(
|
|
performance,
|
|
completionRequest,
|
|
dartdocDirectiveInfo,
|
|
);
|
|
|
|
performance.run('imports', (performance) {
|
|
if (_dartCompletionRequest.includeIdentifiers) {
|
|
_logger.run('Add imported suggestions', () {
|
|
suggestions.addAll(
|
|
_importedLibrariesSuggestions(
|
|
target: resolvedUnit.libraryElement,
|
|
performance: performance,
|
|
),
|
|
);
|
|
});
|
|
}
|
|
});
|
|
|
|
var filter = _FilterSort(
|
|
_dartCompletionRequest,
|
|
suggestions,
|
|
);
|
|
|
|
performance.run('filter', (performance) {
|
|
_logger.run('Filter suggestions', () {
|
|
performance.getDataInt('count').add(suggestions.length);
|
|
suggestions = filter.perform();
|
|
performance.getDataInt('matchCount').add(suggestions.length);
|
|
});
|
|
});
|
|
|
|
var result = CiderCompletionResult._(
|
|
suggestions: suggestions,
|
|
performance: CiderCompletionPerformance._(
|
|
file: Duration.zero,
|
|
imports: performance.getChild('imports')!.elapsed,
|
|
resolution: performance.getChild('resolution')!.elapsed,
|
|
suggestions: performance.getChild('suggestions')!.elapsed,
|
|
operations: _performanceRoot.children.first,
|
|
),
|
|
prefixStart: CiderPosition(line, column - filter._pattern.length),
|
|
);
|
|
|
|
return result;
|
|
});
|
|
}
|
|
|
|
@Deprecated('Use compute')
|
|
Future<CiderCompletionResult> compute2({
|
|
required String path,
|
|
required int line,
|
|
required int column,
|
|
}) async {
|
|
return compute(path: path, line: line, column: column);
|
|
}
|
|
|
|
/// Prepare for computing completions in files from the [pathList].
|
|
///
|
|
/// This method might be called when we are finishing a large initial
|
|
/// analysis, so spending additionally a fraction of this time to make
|
|
/// any subsequent completion seem fast is a reasonable trade-off.
|
|
Future<void> warmUp(List<String> pathList) async {
|
|
for (var path in pathList) {
|
|
await compute(path: path, line: 0, column: 0);
|
|
}
|
|
}
|
|
|
|
/// Return suggestions from libraries imported into the [target].
|
|
///
|
|
/// TODO(scheglov) Implement show / hide combinators.
|
|
/// TODO(scheglov) Implement prefixes.
|
|
List<CompletionSuggestion> _importedLibrariesSuggestions({
|
|
required LibraryElement target,
|
|
required OperationPerformanceImpl performance,
|
|
}) {
|
|
var suggestions = <CompletionSuggestion>[];
|
|
for (var importedLibrary in target.importedLibraries) {
|
|
var importedSuggestions = _importedLibrarySuggestions(
|
|
element: importedLibrary,
|
|
performance: performance,
|
|
);
|
|
suggestions.addAll(importedSuggestions);
|
|
}
|
|
performance.getDataInt('count').add(suggestions.length);
|
|
return suggestions;
|
|
}
|
|
|
|
/// Return cached, or compute unprefixed suggestions for all elements
|
|
/// exported from the library.
|
|
List<CompletionSuggestion> _importedLibrarySuggestions({
|
|
required LibraryElement element,
|
|
required OperationPerformanceImpl performance,
|
|
}) {
|
|
performance.getDataInt('libraryCount').increment();
|
|
|
|
var path = element.source.fullName;
|
|
var signature = _fileResolver.getLibraryLinkedSignature(
|
|
path: path,
|
|
performance: performance,
|
|
);
|
|
|
|
var cacheEntry = _cache._importedLibraries[path];
|
|
if (cacheEntry == null || cacheEntry.signature != signature) {
|
|
performance.getDataInt('libraryCompute').increment();
|
|
computedImportedLibraries.add(path);
|
|
var suggestions = _librarySuggestions(element);
|
|
cacheEntry = _CiderImportedLibrarySuggestions(
|
|
signature,
|
|
suggestions,
|
|
);
|
|
_cache._importedLibraries[path] = cacheEntry;
|
|
}
|
|
return cacheEntry.suggestions;
|
|
}
|
|
|
|
/// Compute all unprefixed suggestions for all elements exported from
|
|
/// the library.
|
|
List<CompletionSuggestion> _librarySuggestions(LibraryElement element) {
|
|
var suggestionBuilder = SuggestionBuilder(_dartCompletionRequest);
|
|
var visitor = LibraryElementSuggestionBuilder(
|
|
_dartCompletionRequest, suggestionBuilder);
|
|
var exportMap = element.exportNamespace.definedNames;
|
|
for (var definedElement in exportMap.values) {
|
|
definedElement.accept(visitor);
|
|
}
|
|
return suggestionBuilder.suggestions.toList();
|
|
}
|
|
}
|
|
|
|
class CiderCompletionPerformance {
|
|
/// The elapsed time for file access.
|
|
@Deprecated('This operation is not performed anymore')
|
|
final Duration file;
|
|
|
|
/// The elapsed time to compute import suggestions.
|
|
@Deprecated("Use 'operations' instead")
|
|
final Duration imports;
|
|
|
|
/// The elapsed time for resolution.
|
|
@Deprecated("Use 'operations' instead")
|
|
final Duration resolution;
|
|
|
|
/// The elapsed time to compute suggestions.
|
|
@Deprecated("Use 'operations' instead")
|
|
final Duration suggestions;
|
|
|
|
/// The tree of operation performances.
|
|
final OperationPerformance operations;
|
|
|
|
CiderCompletionPerformance._({
|
|
required this.file,
|
|
required this.imports,
|
|
required this.resolution,
|
|
required this.suggestions,
|
|
required this.operations,
|
|
});
|
|
}
|
|
|
|
class CiderCompletionResult {
|
|
final List<CompletionSuggestion> suggestions;
|
|
|
|
final CiderCompletionPerformance performance;
|
|
|
|
/// The start of the range that should be replaced with the suggestion. This
|
|
/// position always precedes or is the same as the cursor provided in the
|
|
/// completion request.
|
|
final CiderPosition prefixStart;
|
|
|
|
CiderCompletionResult._({
|
|
required this.suggestions,
|
|
required this.performance,
|
|
required this.prefixStart,
|
|
});
|
|
}
|
|
|
|
class CiderPosition {
|
|
final int line;
|
|
final int column;
|
|
|
|
CiderPosition(this.line, this.column);
|
|
}
|
|
|
|
class _CiderImportedLibrarySuggestions {
|
|
final String signature;
|
|
final List<CompletionSuggestion> suggestions;
|
|
|
|
_CiderImportedLibrarySuggestions(this.signature, this.suggestions);
|
|
}
|
|
|
|
class _FilterSort {
|
|
final DartCompletionRequestImpl _request;
|
|
final List<CompletionSuggestion> _suggestions;
|
|
|
|
late FuzzyMatcher _matcher;
|
|
late String _pattern;
|
|
|
|
_FilterSort(this._request, this._suggestions);
|
|
|
|
List<CompletionSuggestion> perform() {
|
|
_pattern = _request.targetPrefix;
|
|
_matcher = FuzzyMatcher(_pattern, matchStyle: MatchStyle.SYMBOL);
|
|
|
|
var scored = _suggestions
|
|
.map((e) => _FuzzyScoredSuggestion(e, _score(e)))
|
|
.where((e) => e.score > 0)
|
|
.toList();
|
|
|
|
scored.sort((a, b) {
|
|
// Prefer what the user requested by typing.
|
|
if (a.score > b.score) {
|
|
return -1;
|
|
} else if (a.score < b.score) {
|
|
return 1;
|
|
}
|
|
|
|
// Then prefer what is more relevant in the context.
|
|
if (a.suggestion.relevance != b.suggestion.relevance) {
|
|
return -(a.suggestion.relevance - b.suggestion.relevance);
|
|
}
|
|
|
|
// Other things being equal, sort by name.
|
|
return a.suggestion.completion.compareTo(b.suggestion.completion);
|
|
});
|
|
|
|
return scored.map((e) => e.suggestion).toList();
|
|
}
|
|
|
|
double _score(CompletionSuggestion e) {
|
|
var suggestionTextToMatch = e.completion;
|
|
|
|
if (e.kind == CompletionSuggestionKind.NAMED_ARGUMENT) {
|
|
var index = suggestionTextToMatch.indexOf(':');
|
|
if (index != -1) {
|
|
suggestionTextToMatch = suggestionTextToMatch.substring(0, index);
|
|
}
|
|
}
|
|
|
|
return _matcher.score(suggestionTextToMatch);
|
|
}
|
|
}
|
|
|
|
/// [CompletionSuggestion] scored using [FuzzyMatcher].
|
|
class _FuzzyScoredSuggestion {
|
|
final CompletionSuggestion suggestion;
|
|
final double score;
|
|
|
|
_FuzzyScoredSuggestion(this.suggestion, this.score);
|
|
}
|