|
6 | 6 | * found in the LICENSE file at https://angular.io/license
|
7 | 7 | */
|
8 | 8 |
|
9 |
| -import {AbsoluteSourceSpan, AST, AstPath, AttrAst, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, Element, ElementAst, EmptyExpr, ExpressionBinding, getHtmlTagDefinition, HtmlAstPath, Node as HtmlAst, NullTemplateVisitor, ParseSpan, ReferenceAst, TagContentType, TemplateBinding, Text, VariableBinding} from '@angular/compiler'; |
| 9 | +import {AbsoluteSourceSpan, AST, AstPath, AttrAst, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, Element, ElementAst, EmptyExpr, ExpressionBinding, getHtmlTagDefinition, HtmlAstPath, Node as HtmlAst, NullTemplateVisitor, ParseSpan, ReferenceAst, TagContentType, TemplateBinding, Text, VariableBinding, Visitor} from '@angular/compiler'; |
10 | 10 | import {$$, $_, isAsciiLetter, isDigit} from '@angular/compiler/src/chars';
|
11 | 11 |
|
12 | 12 | import {ATTR, getBindingDescriptor} from './binding_utils';
|
@@ -127,79 +127,97 @@ function getBoundedWordSpan(
|
127 | 127 |
|
128 | 128 | export function getTemplateCompletions(
|
129 | 129 | templateInfo: ng.AstResult, position: number): ng.CompletionEntry[] {
|
130 |
| - let result: ng.CompletionEntry[] = []; |
131 | 130 | const {htmlAst, template} = templateInfo;
|
132 |
| - // The templateNode starts at the delimiter character so we add 1 to skip it. |
| 131 | + // Calculate the position relative to the start of the template. This is needed |
| 132 | + // because spans in HTML AST are relative. Inline template has non-zero start position. |
133 | 133 | const templatePosition = position - template.span.start;
|
134 |
| - const path = getPathToNodeAtPosition(htmlAst, templatePosition); |
135 |
| - const mostSpecific = path.tail; |
136 |
| - if (path.empty || !mostSpecific) { |
137 |
| - result = elementCompletions(templateInfo); |
138 |
| - } else { |
139 |
| - const astPosition = templatePosition - mostSpecific.sourceSpan.start.offset; |
140 |
| - mostSpecific.visit( |
141 |
| - { |
142 |
| - visitElement(ast) { |
143 |
| - const startTagSpan = spanOf(ast.sourceSpan); |
144 |
| - const tagLen = ast.name.length; |
145 |
| - // + 1 for the opening angle bracket |
146 |
| - if (templatePosition <= startTagSpan.start + tagLen + 1) { |
147 |
| - // If we are in the tag then return the element completions. |
148 |
| - result = elementCompletions(templateInfo); |
149 |
| - } else if (templatePosition < startTagSpan.end) { |
150 |
| - // We are in the attribute section of the element (but not in an attribute). |
151 |
| - // Return the attribute completions. |
152 |
| - result = attributeCompletionsForElement(templateInfo, ast.name); |
153 |
| - } |
154 |
| - }, |
155 |
| - visitAttribute(ast: Attribute) { |
156 |
| - // An attribute consists of two parts, LHS="RHS". |
157 |
| - // Determine if completions are requested for LHS or RHS |
158 |
| - if (ast.valueSpan && inSpan(templatePosition, spanOf(ast.valueSpan))) { |
159 |
| - // RHS completion |
160 |
| - result = attributeValueCompletions(templateInfo, path); |
161 |
| - } else { |
162 |
| - // LHS completion |
163 |
| - result = attributeCompletions(templateInfo, path); |
164 |
| - } |
165 |
| - }, |
166 |
| - visitText(ast) { |
167 |
| - result = interpolationCompletions(templateInfo, templatePosition); |
168 |
| - if (result.length) return result; |
169 |
| - const element = path.first(Element); |
170 |
| - if (element) { |
171 |
| - const definition = getHtmlTagDefinition(element.name); |
172 |
| - if (definition.contentType === TagContentType.PARSABLE_DATA) { |
173 |
| - result = voidElementAttributeCompletions(templateInfo, path); |
174 |
| - if (!result.length) { |
175 |
| - // If the element can hold content, show element completions. |
176 |
| - result = elementCompletions(templateInfo); |
177 |
| - } |
178 |
| - } |
179 |
| - } else { |
180 |
| - // If no element container, implies parsable data so show elements. |
181 |
| - result = voidElementAttributeCompletions(templateInfo, path); |
182 |
| - if (!result.length) { |
183 |
| - result = elementCompletions(templateInfo); |
184 |
| - } |
185 |
| - } |
186 |
| - }, |
187 |
| - visitComment() {}, |
188 |
| - visitExpansion() {}, |
189 |
| - visitExpansionCase() {} |
190 |
| - }, |
191 |
| - null); |
192 |
| - } |
193 |
| - |
| 134 | + const htmlPath: HtmlAstPath = getPathToNodeAtPosition(htmlAst, templatePosition); |
| 135 | + const mostSpecific = htmlPath.tail; |
| 136 | + const visitor = new HtmlVisitor(templateInfo, htmlPath); |
| 137 | + const results: ng.CompletionEntry[] = mostSpecific ? |
| 138 | + mostSpecific.visit(visitor, null /* context */) : |
| 139 | + elementCompletions(templateInfo); |
194 | 140 | const replacementSpan = getBoundedWordSpan(templateInfo, position, mostSpecific);
|
195 |
| - return result.map(entry => { |
| 141 | + return results.map(entry => { |
196 | 142 | return {
|
197 | 143 | ...entry,
|
198 | 144 | replacementSpan,
|
199 | 145 | };
|
200 | 146 | });
|
201 | 147 | }
|
202 | 148 |
|
| 149 | +class HtmlVisitor implements Visitor { |
| 150 | + /** |
| 151 | + * Position relative to the start of the template. |
| 152 | + */ |
| 153 | + private readonly relativePosition: number; |
| 154 | + constructor(private readonly templateInfo: ng.AstResult, private readonly htmlPath: HtmlAstPath) { |
| 155 | + this.relativePosition = htmlPath.position; |
| 156 | + } |
| 157 | + // Note that every visitor method must explicitly specify return type because |
| 158 | + // Visitor returns `any` for all methods. |
| 159 | + visitElement(ast: Element): ng.CompletionEntry[] { |
| 160 | + const startTagSpan = spanOf(ast.sourceSpan); |
| 161 | + const tagLen = ast.name.length; |
| 162 | + // + 1 for the opening angle bracket |
| 163 | + if (this.relativePosition <= startTagSpan.start + tagLen + 1) { |
| 164 | + // If we are in the tag then return the element completions. |
| 165 | + return elementCompletions(this.templateInfo); |
| 166 | + } |
| 167 | + if (this.relativePosition < startTagSpan.end) { |
| 168 | + // We are in the attribute section of the element (but not in an attribute). |
| 169 | + // Return the attribute completions. |
| 170 | + return attributeCompletionsForElement(this.templateInfo, ast.name); |
| 171 | + } |
| 172 | + return []; |
| 173 | + } |
| 174 | + visitAttribute(ast: Attribute): ng.CompletionEntry[] { |
| 175 | + // An attribute consists of two parts, LHS="RHS". |
| 176 | + // Determine if completions are requested for LHS or RHS |
| 177 | + if (ast.valueSpan && inSpan(this.relativePosition, spanOf(ast.valueSpan))) { |
| 178 | + // RHS completion |
| 179 | + return attributeValueCompletions(this.templateInfo, this.htmlPath); |
| 180 | + } |
| 181 | + // LHS completion |
| 182 | + return attributeCompletions(this.templateInfo, this.htmlPath); |
| 183 | + } |
| 184 | + visitText(): ng.CompletionEntry[] { |
| 185 | + const templatePath = findTemplateAstAt(this.templateInfo.templateAst, this.relativePosition); |
| 186 | + if (templatePath.tail instanceof BoundTextAst) { |
| 187 | + // If we know that this is an interpolation then do not try other scenarios. |
| 188 | + const visitor = new ExpressionVisitor( |
| 189 | + this.templateInfo, this.relativePosition, |
| 190 | + () => |
| 191 | + getExpressionScope(diagnosticInfoFromTemplateInfo(this.templateInfo), templatePath)); |
| 192 | + templatePath.tail?.visit(visitor, null); |
| 193 | + return visitor.results; |
| 194 | + } |
| 195 | + // TODO(kyliau): Not sure if this check is really needed since we don't have |
| 196 | + // any test cases for it. |
| 197 | + const element = this.htmlPath.first(Element); |
| 198 | + if (element && |
| 199 | + getHtmlTagDefinition(element.name).contentType !== TagContentType.PARSABLE_DATA) { |
| 200 | + return []; |
| 201 | + } |
| 202 | + // This is to account for cases like <h1> <a> text | </h1> where the |
| 203 | + // closest element has no closing tag and thus is considered plain text. |
| 204 | + const results = voidElementAttributeCompletions(this.templateInfo, this.htmlPath); |
| 205 | + if (results.length) { |
| 206 | + return results; |
| 207 | + } |
| 208 | + return elementCompletions(this.templateInfo); |
| 209 | + } |
| 210 | + visitComment(): ng.CompletionEntry[] { |
| 211 | + return []; |
| 212 | + } |
| 213 | + visitExpansion(): ng.CompletionEntry[] { |
| 214 | + return []; |
| 215 | + } |
| 216 | + visitExpansionCase(): ng.CompletionEntry[] { |
| 217 | + return []; |
| 218 | + } |
| 219 | +} |
| 220 | + |
203 | 221 | function attributeCompletions(info: ng.AstResult, path: AstPath<HtmlAst>): ng.CompletionEntry[] {
|
204 | 222 | const attr = path.tail;
|
205 | 223 | const elem = path.parentOf(attr);
|
@@ -356,18 +374,6 @@ function elementCompletions(info: ng.AstResult): ng.CompletionEntry[] {
|
356 | 374 | return results;
|
357 | 375 | }
|
358 | 376 |
|
359 |
| -function interpolationCompletions(info: ng.AstResult, position: number): ng.CompletionEntry[] { |
360 |
| - // Look for an interpolation in at the position. |
361 |
| - const templatePath = findTemplateAstAt(info.templateAst, position); |
362 |
| - if (!templatePath.tail) { |
363 |
| - return []; |
364 |
| - } |
365 |
| - const visitor = new ExpressionVisitor( |
366 |
| - info, position, () => getExpressionScope(diagnosticInfoFromTemplateInfo(info), templatePath)); |
367 |
| - templatePath.tail.visit(visitor, null); |
368 |
| - return visitor.results; |
369 |
| -} |
370 |
| - |
371 | 377 | // There is a special case of HTML where text that contains a unclosed tag is treated as
|
372 | 378 | // text. For exaple '<h1> Some <a text </h1>' produces a text nodes inside of the H1
|
373 | 379 | // element "Some <a text". We, however, want to treat this as if the user was requesting
|
|
0 commit comments