| // 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/cider/local_library_contributor.dart'; | |
| import 'package:analysis_server/src/protocol_server.dart'; | |
| import 'package:analysis_server/src/services/completion/dart/completion_manager.dart'; | |
| import 'package:analysis_server/src/services/completion/dart/fuzzy_filter_sort.dart'; | |
| import 'package:analysis_server/src/services/completion/dart/suggestion_builder.dart'; | |
| import 'package:analyzer/dart/element/element.dart'; | |
| import 'package:analyzer/src/dart/analysis/performance_logger.dart'; | |
| import 'package:analyzer/src/dart/analysis/results.dart'; | |
| import 'package:analyzer/src/dart/micro/resolve_file.dart'; | |
| import 'package:analyzer/src/util/performance/operation_performance.dart'; | |
| import 'package:analyzer/src/utilities/extensions/element.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 DartCompletionRequest _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 = []; | |
| new(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(ResolvedForCompletionResultImpl)? testResolvedUnit, | |
| }) { | |
| return _performanceRoot.runAsync('completion', (performance) async { | |
| var resolvedUnit = await performance.runAsync('resolution', ( | |
| performance, | |
| ) { | |
| return _fileResolver.resolveForCompletion( | |
| completionLine: line, | |
| completionColumn: column, | |
| path: path, | |
| performance: performance, | |
| ); | |
| }); | |
| if (testResolvedUnit != null) { | |
| testResolvedUnit(resolvedUnit); | |
| } | |
| var analysisSession = resolvedUnit.analysisSession; | |
| var lineInfo = resolvedUnit.lineInfo; | |
| var offset = lineInfo.getOffsetOfLine(line) + column; | |
| _dartCompletionRequest = DartCompletionRequest( | |
| analysisSession: analysisSession, | |
| fileState: resolvedUnit.fileState, | |
| filePath: resolvedUnit.path, | |
| fileContent: resolvedUnit.content, | |
| libraryFragment: resolvedUnit.libraryFragment, | |
| offset: offset, | |
| unit: resolvedUnit.parsedUnit, | |
| ); | |
| var suggestions = await performance.runAsync('suggestions', ( | |
| performance, | |
| ) async { | |
| var result = await _logger.runAsync('Compute suggestions', () async { | |
| var manager = DartCompletionManager( | |
| budget: CompletionBudget(CompletionBudget.defaultDuration), | |
| skipImports: true, | |
| ); | |
| return await manager.computeSuggestions( | |
| _dartCompletionRequest, | |
| performance, | |
| enableOverrideContributor: false, | |
| enableUriContributor: false, | |
| maxSuggestions: -1, | |
| useFilter: true, | |
| ); | |
| }); | |
| performance.getDataInt('count').add(result.length); | |
| return result.toList(); | |
| }); | |
| performance.run('imports', (performance) { | |
| if (_dartCompletionRequest.includeIdentifiers) { | |
| _logger.run('Add imported suggestions', () { | |
| suggestions.addAll( | |
| _importedLibrariesSuggestions( | |
| target: resolvedUnit.libraryFragment, | |
| performance: performance, | |
| ), | |
| ); | |
| }); | |
| } | |
| }); | |
| var filterPattern = _dartCompletionRequest.targetPrefix; | |
| performance.run('filter', (performance) { | |
| _logger.run('Filter suggestions', () { | |
| performance.getDataInt('count').add(suggestions.length); | |
| suggestions = fuzzyFilterSort( | |
| pattern: filterPattern, | |
| suggestions: suggestions, | |
| ); | |
| performance.getDataInt('matchCount').add(suggestions.length); | |
| }); | |
| }); | |
| var result = CiderCompletionResult._( | |
| suggestions: suggestions.map((e) => e.build()).toList(), | |
| performance: CiderCompletionPerformance._( | |
| operations: _performanceRoot.children.first, | |
| ), | |
| prefixStart: CiderPosition(line, column - filterPattern.length), | |
| ); | |
| return result; | |
| }); | |
| } | |
| /// 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<CompletionSuggestionBuilder> _importedLibrariesSuggestions({ | |
| required LibraryFragment target, | |
| required OperationPerformanceImpl performance, | |
| }) { | |
| var suggestionBuilders = <CompletionSuggestionBuilder>[]; | |
| var importedLibraries = target.withEnclosing2 | |
| .expand((fragment) => fragment.libraryImports) | |
| .map((import) => import.importedLibrary) | |
| .nonNulls | |
| .toSet(); | |
| for (var importedLibrary in importedLibraries) { | |
| var importedSuggestions = _importedLibrarySuggestions( | |
| element: importedLibrary, | |
| performance: performance, | |
| ); | |
| suggestionBuilders.addAll(importedSuggestions); | |
| } | |
| performance.getDataInt('count').add(suggestionBuilders.length); | |
| return suggestionBuilders; | |
| } | |
| /// Return cached, or compute unprefixed suggestions for all elements | |
| /// exported from the library. | |
| List<CompletionSuggestionBuilder> _importedLibrarySuggestions({ | |
| required LibraryElement element, | |
| required OperationPerformanceImpl performance, | |
| }) { | |
| performance.getDataInt('libraryCount').increment(); | |
| var path = element.firstFragment.libraryFragment!.source.fullName; | |
| var signature = _fileResolver.getLibraryLinkedSignature(path); | |
| 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.suggestionBuilders; | |
| } | |
| /// Compute all unprefixed suggestions for all elements exported from | |
| /// the library. | |
| List<CompletionSuggestionBuilder> _librarySuggestions( | |
| LibraryElement element, | |
| ) { | |
| var suggestionBuilder = SuggestionBuilder( | |
| _dartCompletionRequest, | |
| useFilter: false, | |
| ); | |
| suggestionBuilder.libraryUriStr = element.uri.toString(); | |
| var visitor = LibraryElementSuggestionBuilder( | |
| _dartCompletionRequest, | |
| suggestionBuilder, | |
| ); | |
| var exportMap = element.exportNamespace.definedNames2; | |
| for (var definedElement in exportMap.values) { | |
| definedElement.accept(visitor); | |
| } | |
| return suggestionBuilder.suggestions.toList(); | |
| } | |
| } | |
| class CiderCompletionPerformance { | |
| /// The tree of operation performances. | |
| final OperationPerformance operations; | |
| new _({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; | |
| new _({ | |
| required this.suggestions, | |
| required this.performance, | |
| required this.prefixStart, | |
| }); | |
| } | |
| class CiderPosition { | |
| final int line; | |
| final int column; | |
| new(this.line, this.column); | |
| } | |
| class _CiderImportedLibrarySuggestions { | |
| final String signature; | |
| final List<CompletionSuggestionBuilder> suggestionBuilders; | |
| new(this.signature, this.suggestionBuilders); | |
| } |