blob: a1accfc616c9126def6d74211acd617dd363a30b [file] [log] [blame]
import 'dart:async';
import 'dart:collection';
import 'package:analysis_server/plugin/protocol/protocol.dart' as protocol
show Element, ElementKind;
import 'package:analysis_server/src/provisional/completion/completion_core.dart';
import 'package:analysis_server/src/provisional/completion/dart/completion_dart.dart';
import 'package:analysis_server/src/services/completion/completion_core.dart';
import 'package:analysis_server/src/services/completion/dart/completion_manager.dart';
import 'package:analysis_server/src/services/completion/dart/optype.dart';
import 'package:analysis_server/src/services/completion/dart/type_member_contributor.dart';
import 'package:analysis_server/src/services/completion/dart/inherited_reference_contributor.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:angular_analyzer_plugin/src/converter.dart';
import 'package:angular_analyzer_plugin/src/model.dart';
import 'package:angular_analyzer_plugin/src/selector.dart';
import 'package:angular_analyzer_plugin/ast.dart';
import 'package:analysis_server/src/protocol_server.dart'
show CompletionSuggestion, CompletionSuggestionKind, Location;
import 'package:analysis_server/src/protocol_server.dart'
show CompletionSuggestion;
import 'embedded_dart_completion_request.dart';
bool offsetContained(int offset, int start, int length) {
return start <= offset && start + length >= offset;
}
AngularAstNode findTarget(int offset, AngularAstNode root) {
for (AngularAstNode child in root.children) {
if (child is ElementInfo) {
if (child.isSynthetic) {
var target = findTarget(offset, child);
if (!(target is ElementInfo && target.openingSpan == null)) {
return target;
}
} else {
if (offsetContained(offset, child.openingNameSpan.offset,
child.openingNameSpan.length)) {
return child;
} else if (offsetContained(offset, child.offset, child.length)) {
return findTarget(offset, child);
}
}
} else if (offsetContained(offset, child.offset, child.length)) {
return findTarget(offset, child);
}
}
return root;
}
class DartSnippetExtractor extends AngularAstVisitor {
AstNode dartSnippet = null;
int offset;
@override
visitDocumentInfo(DocumentInfo document) {}
// don't recurse, findTarget already did that
@override
visitElementInfo(ElementInfo element) {}
@override
visitTextAttr(TextAttribute attr) {}
@override
visitExpressionBoundAttr(ExpressionBoundAttribute attr) {
if (attr.expression != null &&
offsetContained(
offset, attr.expression.offset, attr.expression.length)) {
dartSnippet = attr.expression;
}
}
@override
visitStatementsBoundAttr(StatementsBoundAttribute attr) {
for (Statement statement in attr.statements) {
if (offsetContained(offset, statement.offset, statement.length)) {
dartSnippet = statement;
}
}
}
@override
visitMustache(Mustache mustache) {
if (offsetContained(
offset, mustache.exprBegin, mustache.exprEnd - mustache.exprBegin)) {
dartSnippet = mustache.expression;
}
}
@override
visitTemplateAttr(TemplateAttribute attr) {
// if we visit this, we're in a template but after one of its attributes.
AttributeInfo attributeToAppendTo;
for (AttributeInfo subAttribute in attr.virtualAttributes) {
if (subAttribute.valueOffset == null && subAttribute.offset < offset) {
attributeToAppendTo = subAttribute;
}
}
if (attributeToAppendTo != null &&
attributeToAppendTo is TextAttribute &&
!attributeToAppendTo.name.startsWith("let")) {
AnalysisErrorListener analysisErrorListener =
new IgnoringAnalysisErrorListener();
EmbeddedDartParser dartParser =
new EmbeddedDartParser(null, analysisErrorListener, null);
dartSnippet = dartParser.parseDartExpression(offset, '', false);
}
}
}
class IgnoringAnalysisErrorListener implements AnalysisErrorListener {
@override
void onError(AnalysisError error) {}
}
class LocalVariablesExtractor extends AngularAstVisitor {
Map<String, LocalVariable> variables = null;
// don't recurse, findTarget already did that
@override
visitDocumentInfo(DocumentInfo document) {}
@override
visitElementInfo(ElementInfo element) {}
@override
visitTextAttr(TextAttribute attr) {}
@override
visitExpressionBoundAttr(ExpressionBoundAttribute attr) {
variables = attr.localVariables;
}
@override
visitStatementsBoundAttr(StatementsBoundAttribute attr) {
variables = attr.localVariables;
}
@override
visitMustache(Mustache mustache) {
variables = mustache.localVariables;
}
}
class ReplacementRangeCalculator extends AngularAstVisitor {
CompletionRequestImpl request;
ReplacementRangeCalculator(this.request);
@override
visitDocumentInfo(DocumentInfo document) {}
// don't recurse, findTarget already did that
@override
visitElementInfo(ElementInfo element) {
if (element.openingSpan == null) {
return;
}
int nameSpanEnd =
element.openingNameSpan.offset + element.openingNameSpan.length;
if (offsetContained(request.offset, element.openingSpan.offset,
nameSpanEnd - element.openingSpan.offset)) {
request.replacementOffset = element.openingSpan.offset;
request.replacementLength = element.localName.length + 1;
}
}
@override
visitTextAttr(TextAttribute attr) {}
@override
visitTextInfo(TextInfo textInfo) {
if (request.offset > textInfo.offset &&
textInfo.text[request.offset - textInfo.offset - 1] == '<') {
request.replacementOffset--;
request.replacementLength = 1;
}
}
@override
visitExpressionBoundAttr(ExpressionBoundAttribute attr) {
if (offsetContained(
request.offset, attr.originalNameOffset, attr.originalName.length)) {
request.replacementOffset = attr.originalNameOffset;
request.replacementLength = attr.originalName.length;
}
}
@override
visitStatementsBoundAttr(StatementsBoundAttribute attr) {
if (offsetContained(
request.offset, attr.originalNameOffset, attr.originalName.length)) {
request.replacementOffset = attr.originalNameOffset;
request.replacementLength = attr.originalName.length;
}
}
@override
visitMustache(Mustache mustache) {}
}
class AngularDartCompletionContributor extends CompletionContributor {
/**
* Return a [Future] that completes with a list of suggestions
* for the given completion [request].
*/
Future<List<CompletionSuggestion>> computeSuggestions(
CompletionRequest request) async {
if (!request.source.shortName.endsWith('.dart')) {
return [];
}
//return new TemplateCompleter().computeSuggestions(
// request, templates, standardHtmlEvents, standardHtmlAttributes);
return [];
}
}
class AngularTemplateCompletionContributor extends CompletionContributor {
/**
* Return a [Future] that completes with a list of suggestions
* for the given completion [request]. This will
* throw [AbortCompletion] if the completion request has been aborted.
*/
Future<List<CompletionSuggestion>> computeSuggestions(
CompletionRequest request) async {
if (request.source.shortName.endsWith('.html')) {
//return new TemplateCompleter().computeSuggestions(
// request, templates, standardHtmlEvents, standardHtmlAttributes);
}
return [];
}
}
class TemplateCompleter {
static const int RELEVANCE_TRANSCLUSION = DART_RELEVANCE_DEFAULT + 10;
Future<List<CompletionSuggestion>> computeSuggestions(
CompletionRequest request,
List<Template> templates,
List<OutputElement> standardHtmlEvents,
List<InputElement> standardHtmlAttributes) async {
var suggestions = <CompletionSuggestion>[];
for (Template template in templates) {
var target = findTarget(request.offset, template.ast);
target.accept(new ReplacementRangeCalculator(request));
var extractor = new DartSnippetExtractor();
extractor.offset = request.offset;
target.accept(extractor);
// If [CompletionRequest] is in
// [StatementsBoundAttribute],
// [ExpressionsBoundAttribute],
// [Mustache],
// [TemplateAttribute].
if (extractor.dartSnippet != null) {
var dartRequest = new EmbeddedDartCompletionRequest.from(
request, extractor.dartSnippet);
var range = new ReplacementRange.compute(
dartRequest.offset, dartRequest.target);
(request as CompletionRequestImpl)
..replacementOffset = range.offset
..replacementLength = range.length;
dartRequest.libraryElement = template.view.classElement.library;
var memberContributor = new TypeMemberContributor();
var inheritedContributor = new InheritedReferenceContributor();
suggestions.addAll(
inheritedContributor.computeSuggestionsForClass(
template.view.classElement,
dartRequest,
skipChildClass: false,
),
);
suggestions
.addAll(await memberContributor.computeSuggestions(dartRequest));
if (dartRequest.opType.includeIdentifiers) {
var varExtractor = new LocalVariablesExtractor();
target.accept(varExtractor);
if (varExtractor.variables != null) {
addLocalVariables(
suggestions,
varExtractor.variables,
dartRequest.opType,
);
}
}
} else if (target is ElementInfo) {
if (target.closingSpan != null &&
offsetContained(request.offset, target.closingSpan.offset,
target.closingSpan.length)) {
if (request.offset ==
(target.closingSpan.offset + target.closingSpan.length)) {
// In closing tag, but could be directly after it; ex: '</div>^'.
suggestHtmlTags(template, suggestions);
if (target.parent != null || target.parent is! DocumentInfo) {
suggestTransclusions(target.parent, suggestions);
}
} else {
// Directly within closing tag; suggest nothing. Ex: '</div^>'
continue;
}
}
if (!offsetContained(request.offset, target.openingNameSpan.offset,
target.openingNameSpan.length)) {
// If request is not in [openingNameSpan], suggest decorators.
suggestInputs(target.boundDirectives, suggestions,
standardHtmlAttributes, target.boundStandardInputs);
suggestOutputs(target.boundDirectives, suggestions,
standardHtmlEvents, target.boundStandardOutputs);
} else {
// Otherwise, suggest HTML tags and transclusions.
suggestHtmlTags(template, suggestions);
if (target.parent != null || target.parent is! DocumentInfo) {
suggestTransclusions(target.parent, suggestions);
}
}
} else if (target is ExpressionBoundAttribute &&
target.bound == ExpressionBoundType.input &&
offsetContained(request.offset, target.originalNameOffset,
target.originalName.length)) {
suggestInputs(target.parent.boundDirectives, suggestions,
standardHtmlAttributes, target.parent.boundStandardInputs,
currentAttr: target);
} else if (target is StatementsBoundAttribute) {
suggestOutputs(target.parent.boundDirectives, suggestions,
standardHtmlEvents, target.parent.boundStandardOutputs,
currentAttr: target);
} else if (target is TemplateAttribute) {
suggestInputs(target.parent.boundDirectives, suggestions,
standardHtmlAttributes, target.parent.boundStandardInputs);
suggestOutputs(target.parent.boundDirectives, suggestions,
standardHtmlEvents, target.parent.boundStandardOutputs);
} else if (target is TextAttribute &&
target.nameOffset != null &&
offsetContained(
request.offset, target.nameOffset, target.name.length)) {
suggestInputs(target.parent.boundDirectives, suggestions,
standardHtmlAttributes, target.parent.boundStandardInputs);
suggestOutputs(target.parent.boundDirectives, suggestions,
standardHtmlEvents, target.parent.boundStandardOutputs);
} else if (target is TextInfo) {
suggestHtmlTags(template, suggestions);
suggestTransclusions(target.parent, suggestions);
}
}
return suggestions;
}
suggestTransclusions(
ElementInfo container, List<CompletionSuggestion> suggestions) {
for (AbstractDirective directive in container.directives) {
if (directive is! Component) {
continue;
}
Component component = directive;
Template template = component?.view?.template;
if (template == null) {
continue;
}
for (NgContent ngContent in component.ngContents) {
if (ngContent.selector == null) {
continue;
}
List<HtmlTagForSelector> tags = ngContent.selector.suggestTags();
for (HtmlTagForSelector tag in tags) {
Location location = new Location(
template.view.templateSource.fullName,
ngContent.offset,
ngContent.length,
0,
0);
suggestions.add(_createHtmlTagSuggestion(
tag.toString(),
RELEVANCE_TRANSCLUSION,
_createHtmlTagTransclusionElement(tag.toString(),
protocol.ElementKind.CLASS_TYPE_ALIAS, location)));
}
}
}
}
suggestHtmlTags(Template template, List<CompletionSuggestion> suggestions) {
Map<String, List<AbstractDirective>> elementTagMap =
template.view.elementTagsInfo;
for (String elementTagName in elementTagMap.keys) {
CompletionSuggestion currentSuggestion = _createHtmlTagSuggestion(
'<' + elementTagName,
DART_RELEVANCE_DEFAULT,
_createHtmlTagElement(
elementTagName,
elementTagMap[elementTagName].first,
protocol.ElementKind.CLASS_TYPE_ALIAS));
if (currentSuggestion != null) {
suggestions.add(currentSuggestion);
}
}
}
suggestInputs(
List<DirectiveBinding> directives,
List<CompletionSuggestion> suggestions,
List<InputElement> standardHtmlAttributes,
List<InputBinding> boundStandardAttributes,
{ExpressionBoundAttribute currentAttr}) {
for (DirectiveBinding directive in directives) {
Set<InputElement> usedInputs = new HashSet.from(directive.inputBindings
.where((b) => b.attribute != currentAttr)
.map((b) => b.boundInput));
for (InputElement input in directive.boundDirective.inputs) {
// don't recommend [name] [name] [name]
if (usedInputs.contains(input)) {
continue;
}
suggestions.add(_createInputSuggestion(input, DART_RELEVANCE_DEFAULT,
_createInputElement(input, protocol.ElementKind.SETTER)));
}
}
Set<InputElement> usedStdInputs = new HashSet.from(boundStandardAttributes
.where((b) => b.attribute != currentAttr)
.map((b) => b.boundInput));
for (InputElement input in standardHtmlAttributes) {
// TODO don't recommend [hidden] [hidden] [hidden]
if (usedStdInputs.contains(input)) {
continue;
}
suggestions.add(_createInputSuggestion(input, DART_RELEVANCE_DEFAULT - 1,
_createInputElement(input, protocol.ElementKind.SETTER)));
}
}
suggestOutputs(
List<DirectiveBinding> directives,
List<CompletionSuggestion> suggestions,
List<OutputElement> standardHtmlEvents,
List<OutputBinding> boundStandardOutputs,
{BoundAttributeInfo currentAttr}) {
for (DirectiveBinding directive in directives) {
Set<OutputElement> usedOutputs = new HashSet.from(directive.outputBindings
.where((b) => b.attribute != currentAttr)
.map((b) => b.boundOutput));
for (OutputElement output in directive.boundDirective.outputs) {
// don't recommend (close) (close) (close)
if (usedOutputs.contains(output)) {
continue;
}
suggestions.add(_createOutputSuggestion(output, DART_RELEVANCE_DEFAULT,
_createOutputElement(output, protocol.ElementKind.GETTER)));
}
}
Set<OutputElement> usedStdOutputs = new HashSet.from(boundStandardOutputs
.where((b) => b.attribute != currentAttr)
.map((b) => b.boundOutput));
for (OutputElement output in standardHtmlEvents) {
// don't recommend (click) (click) (click)
if (usedStdOutputs.contains(output)) {
continue;
}
suggestions.add(_createOutputSuggestion(
output,
DART_RELEVANCE_DEFAULT - 1, // just below regular relevance
_createOutputElement(output, protocol.ElementKind.GETTER)));
}
}
addLocalVariables(List<CompletionSuggestion> suggestions,
Map<String, LocalVariable> localVars, OpType optype) {
for (LocalVariable eachVar in localVars.values) {
suggestions.add(_addLocalVariableSuggestion(
eachVar,
eachVar.dartVariable.type,
protocol.ElementKind.LOCAL_VARIABLE,
optype,
relevance: DART_RELEVANCE_LOCAL_VARIABLE));
}
}
CompletionSuggestion _addLocalVariableSuggestion(LocalVariable variable,
DartType typeName, protocol.ElementKind elemKind, OpType optype,
{int relevance: DART_RELEVANCE_DEFAULT}) {
relevance = optype.returnValueSuggestionsFilter(
variable.dartVariable.type, relevance) ??
DART_RELEVANCE_DEFAULT;
return _createLocalSuggestion(variable, relevance, typeName,
_createLocalElement(variable, elemKind, typeName));
}
CompletionSuggestion _createLocalSuggestion(LocalVariable localVar,
int defaultRelevance, DartType type, protocol.Element element) {
String completion = localVar.name;
return new CompletionSuggestion(CompletionSuggestionKind.INVOCATION,
defaultRelevance, completion, completion.length, 0, false, false,
returnType: type.toString(), element: element);
}
protocol.Element _createLocalElement(
LocalVariable localVar, protocol.ElementKind kind, DartType type) {
String name = localVar.name;
Location location = new Location(localVar.source.fullName,
localVar.nameOffset, localVar.nameLength, 0, 0);
int flags = protocol.Element.makeFlags();
return new protocol.Element(kind, name, flags,
location: location, returnType: type.toString());
}
CompletionSuggestion _createHtmlTagSuggestion(
String elementTagName, int defaultRelevance, protocol.Element element) {
return new CompletionSuggestion(
CompletionSuggestionKind.INVOCATION,
defaultRelevance,
elementTagName,
elementTagName.length,
0,
false,
false,
element: element);
}
protocol.Element _createHtmlTagElement(String elementTagName,
AbstractDirective directive, protocol.ElementKind kind) {
ElementNameSelector selector = directive.elementTags.firstWhere(
(currSelector) => currSelector.toString() == elementTagName);
int offset = selector.nameElement.nameOffset;
int length = selector.nameElement.nameLength;
Location location =
new Location(directive.source.fullName, offset, length, 0, 0);
int flags = protocol.Element
.makeFlags(isAbstract: false, isDeprecated: false, isPrivate: false);
return new protocol.Element(kind, '<' + elementTagName, flags,
location: location);
}
protocol.Element _createHtmlTagTransclusionElement(
String elementTagName, protocol.ElementKind kind, Location location) {
int flags = protocol.Element
.makeFlags(isAbstract: false, isDeprecated: false, isPrivate: false);
return new protocol.Element(kind, elementTagName, flags,
location: location);
}
CompletionSuggestion _createInputSuggestion(InputElement inputElement,
int defaultRelevance, protocol.Element element) {
String completion = '[' + inputElement.name + ']';
return new CompletionSuggestion(CompletionSuggestionKind.INVOCATION,
defaultRelevance, completion, completion.length, 0, false, false,
element: element);
}
protocol.Element _createInputElement(
InputElement inputElement, protocol.ElementKind kind) {
String name = '[' + inputElement.name + ']';
Location location = new Location(inputElement.source.fullName,
inputElement.nameOffset, inputElement.nameLength, 0, 0);
int flags = protocol.Element
.makeFlags(isAbstract: false, isDeprecated: false, isPrivate: false);
return new protocol.Element(kind, name, flags, location: location);
}
CompletionSuggestion _createOutputSuggestion(OutputElement outputElement,
int defaultRelevance, protocol.Element element) {
String completion = '(' + outputElement.name + ')';
return new CompletionSuggestion(CompletionSuggestionKind.INVOCATION,
defaultRelevance, completion, completion.length, 0, false, false,
element: element, returnType: outputElement.eventType.toString());
}
protocol.Element _createOutputElement(
OutputElement outputElement, protocol.ElementKind kind) {
String name = '(' + outputElement.name + ')';
Location location = new Location(outputElement.source.fullName,
outputElement.nameOffset, outputElement.nameLength, 0, 0);
int flags = protocol.Element.makeFlags();
return new protocol.Element(kind, name, flags,
location: location, returnType: outputElement.eventType.toString());
}
}