blob: 81da4fa2108f97d2042a3dae71a8e38605406f72 [file]
// 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/pub/pub_package_service.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:path/path.dart' as path;
/// An object that represents the location of a Boolean value.
class BooleanProducer extends Producer {
/// Initialize a location whose valid values are Booleans.
const new();
@override
List<CompletionSuggestion> suggestions(YamlCompletionRequest request) {
return [identifier('true'), identifier('false')];
}
}
/// An object that represents the location of an arbitrary value. They serve as
/// placeholders when there are no reasonable suggestions for a given location.
class EmptyProducer extends Producer {
/// Initialize a location whose valid values are arbitrary.
const new();
@override
List<CompletionSuggestion> suggestions(YamlCompletionRequest request) {
// Returns nothing.
// See https://github.com/dart-lang/sdk/issues/51806#issuecomment-4736379661
// for why this is faster than `Iterable`.
return const [];
}
}
/// An object that represents the location of a value from a finite set of
/// choices.
class EnumProducer extends Producer {
/// The list of valid values at this location.
final List<String> values;
/// Initialize a location whose valid values are in the list of [values].
const new(this.values);
@override
List<CompletionSuggestion> suggestions(YamlCompletionRequest request) {
return [for (var value in values) identifier(value)];
}
}
/// An object that represents the location of a possibly relative file path.
class FilePathProducer extends Producer {
/// Initialize a producer whose valid values are file paths.
const new();
@override
List<CompletionSuggestion> suggestions(YamlCompletionRequest request) {
//
// This currently assumes that all of the paths in the assets section will
// be posix paths.
//
var context = path.posix;
var separator = context.separator;
var precedingText = request.precedingText;
String parentDirectory;
if (precedingText.isEmpty || precedingText.endsWith(separator)) {
parentDirectory = precedingText;
} else {
parentDirectory = context.dirname(precedingText);
}
if (parentDirectory == '.') {
parentDirectory = '';
} else if (parentDirectory.endsWith(separator)) {
parentDirectory = parentDirectory.substring(
0,
parentDirectory.length - separator.length,
);
}
//
// Convert from posix to the platform context.
//
var provider = request.resourceProvider;
context = provider.pathContext;
parentDirectory = context.joinAll(path.posix.split(parentDirectory));
//
// Resolve the relative path and access the disk to see what child entities
// exist within the [parentDirectory] that can be suggested.
//
if (context.isRelative(parentDirectory)) {
parentDirectory = context.join(
context.dirname(request.filePath),
parentDirectory,
);
}
parentDirectory = context.normalize(parentDirectory);
var dir = provider.getResource(parentDirectory);
if (dir is Folder) {
try {
var list = <CompletionSuggestion>[];
for (var child in dir.getChildren()) {
var name = child.shortName;
var relevance = name.startsWith('.') ? 500 : 1000;
list.add(identifier(name, relevance: relevance));
}
return list;
} on FileSystemException {
// Guard against I/O exceptions.
}
}
return const [];
}
}
/// An object that represents the location of the keys/values in a map.
abstract class KeyValueProducer extends Producer {
/// Initialize a producer representing a key/value pair in a map.
const new();
/// Returns a producer for values of the given [key], or `null` if there is
/// no registered producer for the [key].
Producer? producerForKey(String key);
}
/// An object that represents the location of a value that can be expressed
/// either as an element of a list or as a key in a map.
///
/// For example, the lint rules in an analysis options file can be written
/// either as a list of rule names or as a map from rule names to Booleans.
class ListOrMapProducer extends ListProducer implements KeyValueProducer {
/// The producer used to produce suggestions for the value of a key when the
/// map form is used.
final Producer mapValue;
/// Initialize a location whose valid values are either the elements of a
/// list, as determined by the [element] producer, or the keys of a map whose
/// values are determined by the [mapValue] producer.
const new(super.element, {required this.mapValue});
/// A producer that suggests the suggestions of [element] as map keys.
Producer get keyProducer => _MapKeyProducer(element);
@override
Producer? producerForKey(String key) => mapValue;
@override
List<CompletionSuggestion> suggestions(YamlCompletionRequest request) {
// Neither form has been started yet — suggest both list (`- rule`) and
// map (`rule: `) forms so the user can choose which style to begin with.
return [...super.suggestions(request), ...keyProducer.suggestions(request)];
}
}
/// An object that represents the location of an element in a list.
class ListProducer extends Producer {
/// The producer used to produce suggestions for an element of the list.
final Producer element;
/// Initialize a location whose valid values are determined by the [element]
/// producer.
const new(this.element);
@override
List<CompletionSuggestion> suggestions(YamlCompletionRequest request) {
// This method is only called when the cursor is NOT already inside a list
// item (the path didn't traverse a YamlList). When the cursor IS inside
// an item (after `- `), `element.suggestions()` is called directly by
// `_producerForPath`. Therefore we always need the `- ` prefix here.
return [
for (var suggestion in element.suggestions(request))
identifier(
'- ${suggestion.completion}',
relevance: suggestion.relevance,
docComplete: suggestion.docComplete,
),
];
}
}
/// An object that represents the location of the keys in a map.
class MapProducer extends KeyValueProducer {
/// A table from the value of a key to the producer used to make suggestions
/// for the value following the key.
final Map<String, Producer> _children;
/// Initialize a location whose valid values are the keys of a map as encoded
/// by the map of [_children].
const new(this._children);
@override
Producer? producerForKey(String key) => _children[key];
@override
List<CompletionSuggestion> suggestions(YamlCompletionRequest request) {
return [
for (var entry in _children.entries)
if (entry.value is ListProducer)
identifier('${entry.key}:')
else
identifier('${entry.key}: '),
];
}
}
/// An object that represents a specific location in the structure of the valid
/// YAML representation and can produce completion suggestions appropriate for
/// that location.
abstract class Producer {
/// Initialize a newly created instance of this class.
const new();
/// A utility method used to create a suggestion for the [identifier].
CompletionSuggestion identifier(
String identifier, {
int relevance = 1000,
String? docComplete,
}) => CompletionSuggestion(
CompletionSuggestionKind.IDENTIFIER,
relevance,
identifier,
identifier.length,
0,
false,
false,
docComplete: docComplete,
);
/// A utility method used to create a suggestion for the package [packageName].
CompletionSuggestion packageName(
String packageName, {
int relevance = 1000,
}) => CompletionSuggestion(
CompletionSuggestionKind.PACKAGE_NAME,
relevance,
packageName,
packageName.length,
0,
false,
false,
);
/// Return the completion suggestions appropriate to this location.
Iterable<CompletionSuggestion> suggestions(YamlCompletionRequest request);
}
/// The information provided to a [Producer] when requesting completions.
class YamlCompletionRequest {
/// The resource provider used to access the file system.
final ResourceProvider resourceProvider;
/// The Pub package service used for looking up package names/versions.
final PubPackageService? pubPackageService;
/// The absolute path of the file in which completions are being requested.
final String filePath;
/// The text to the left of the cursor.
final String precedingText;
/// Initialize a newly created completion request.
new({
required this.filePath,
required this.precedingText,
required this.resourceProvider,
required this.pubPackageService,
});
}
/// An object that suggests the suggestions of another producer as map keys.
class _MapKeyProducer extends Producer {
/// The producer whose suggestions are to be suggested as map keys.
final Producer element;
/// Initialize a location whose valid values are the suggestions of the
/// [element] producer, written as map keys.
const new(this.element);
@override
List<CompletionSuggestion> suggestions(YamlCompletionRequest request) {
return [
for (var suggestion in element.suggestions(request))
identifier(
'${suggestion.completion}: ',
relevance: suggestion.relevance,
docComplete: suggestion.docComplete,
),
];
}
}