blob: cbb8337d54dc180adb518997bd1c986e927ed1cc [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 'package:analysis_server/src/provisional/completion/dart/completion_dart.dart';
import 'package:analysis_server/src/services/completion/dart/suggestion_builder.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:path/path.dart' show posix;
/// A contributor that produces suggestions based on the content of the file
/// system to complete within URIs in import, export and part directives.
class UriContributor extends DartCompletionContributor {
UriContributor(
DartCompletionRequest request,
SuggestionBuilder builder,
) : super(request, builder);
@override
Future<void> computeSuggestions() async {
var visitor = _UriSuggestionBuilder(request, builder);
request.target.containingNode.accept(visitor);
}
}
class _UriSuggestionBuilder extends SimpleAstVisitor<void> {
final DartCompletionRequest request;
final SuggestionBuilder builder;
_UriSuggestionBuilder(this.request, this.builder);
@override
void visitExportDirective(ExportDirective node) {
visitNamespaceDirective(node);
}
@override
void visitImportDirective(ImportDirective node) {
visitNamespaceDirective(node);
}
void visitNamespaceDirective(NamespaceDirective node) {
var uri = node.uri;
if (uri is SimpleStringLiteral) {
var offset = request.offset;
var start = uri.offset;
var end = uri.end;
if (offset > start) {
if (offset < end) {
// Quoted non-empty string
visitSimpleStringLiteral(uri);
} else if (offset == end) {
if (end == start + 1) {
// Quoted empty string
visitSimpleStringLiteral(uri);
} else {
var data = request.sourceContents!;
if (end == data.length) {
var ch = data[end - 1];
if (ch != '"' && ch != "'") {
// Insertion point at end of file
// and missing closing quote on non-empty string
visitSimpleStringLiteral(uri);
}
}
}
}
} else if (offset == start && offset == end) {
var data = request.sourceContents!;
if (end == data.length) {
var ch = data[end - 1];
if (ch == '"' || ch == "'") {
// Insertion point at end of file
// and missing closing quote on empty string
visitSimpleStringLiteral(uri);
}
}
}
}
}
@override
void visitSimpleStringLiteral(SimpleStringLiteral node) {
var parent = node.parent;
if (parent is NamespaceDirective && parent.uri == node) {
var partialUri = _extractPartialUri(node);
if (partialUri != null) {
_addDartSuggestions();
_addPackageSuggestions(partialUri);
_addFileSuggestions(partialUri);
}
} else if (parent is PartDirective && parent.uri == node) {
var partialUri = _extractPartialUri(node);
if (partialUri != null) {
_addFileSuggestions(partialUri);
}
}
}
void _addDartSuggestions() {
builder.suggestUri('dart:');
var factory = request.sourceFactory;
for (var lib in factory.dartSdk!.sdkLibraries) {
if (!lib.isInternal && !lib.isImplementation) {
if (!lib.shortName.startsWith('dart:_')) {
builder.suggestUri(lib.shortName);
}
}
}
}
void _addFileSuggestions(String partialUri) {
var resProvider = request.resourceProvider;
var resContext = resProvider.pathContext;
var source = request.source;
String parentUri;
if (partialUri.endsWith('/')) {
parentUri = partialUri;
} else {
parentUri = posix.dirname(partialUri);
if (parentUri != '.' && !parentUri.endsWith('/')) {
parentUri = '$parentUri/';
}
}
var uriPrefix = parentUri == '.' ? '' : parentUri;
// Only handle file uris in the format file:///xxx or /xxx
var parentUriScheme = Uri.parse(parentUri).scheme;
if (!parentUri.startsWith('file://') && parentUriScheme != '') {
return;
}
var dirPath = resProvider.pathContext.fromUri(parentUri);
dirPath = resContext.normalize(dirPath);
if (resContext.isRelative(dirPath)) {
var sourceDirPath = resContext.dirname(source.fullName);
if (resContext.isAbsolute(sourceDirPath)) {
dirPath = resContext.normalize(resContext.join(sourceDirPath, dirPath));
} else {
return;
}
// Do not suggest relative paths reaching outside the 'lib' directory.
var srcInLib = resContext.split(sourceDirPath).contains('lib');
var dstInLib = resContext.split(dirPath).contains('lib');
if (srcInLib && !dstInLib) {
return;
}
}
if (dirPath.endsWith('\\.')) {
dirPath = dirPath.substring(0, dirPath.length - 1);
}
var dir = resProvider.getResource(dirPath);
if (dir is Folder) {
try {
for (var child in dir.getChildren()) {
String? completion;
if (child is Folder) {
if (!child.shortName.startsWith('.')) {
completion = '$uriPrefix${child.shortName}/';
}
} else if (child is File) {
if (child.shortName.endsWith('.dart')) {
completion = '$uriPrefix${child.shortName}';
}
}
if (completion != null && completion != source.shortName) {
builder.suggestUri(completion);
}
}
} on FileSystemException {
// Guard against I/O exceptions.
}
}
}
void _addPackageFolderSuggestions(
String partial, String prefix, Folder folder) {
try {
for (var child in folder.getChildren()) {
if (child is Folder) {
var childPrefix = '$prefix${child.shortName}/';
builder.suggestUri(childPrefix);
if (partial.startsWith(childPrefix)) {
_addPackageFolderSuggestions(partial, childPrefix, child);
}
} else {
builder.suggestUri('$prefix${child.shortName}');
}
}
} on FileSystemException {
// Guard against I/O exceptions.
return;
}
}
void _addPackageSuggestions(String partial) {
var factory = request.sourceFactory;
var packageMap = factory.packageMap;
if (packageMap != null) {
builder.suggestUri('package:');
packageMap.forEach((pkgName, folders) {
var prefix = 'package:$pkgName/';
builder.suggestUri(prefix);
for (var folder in folders) {
if (folder.exists) {
_addPackageFolderSuggestions(partial, prefix, folder);
}
}
});
}
}
String? _extractPartialUri(SimpleStringLiteral node) {
if (request.offset < node.contentsOffset) {
return null;
}
return node.literal.lexeme.substring(
node.contentsOffset - node.offset, request.offset - node.offset);
}
}