mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
fix(language-service): Autocomplete block keywords in more cases (#52198)
Previously, autocompletions were not available in two main cases. We correct them. 1. Autocompletions immediately after `@` were usually not working, for example `foo @|`. We fix this by causing the lexer to not consider the `@` part of the text node. 2. Autocompletions such as `@\nfoo`, where a newline follows a bare `@`, were not working because the language service visitor considered us inside the subsequent text node. We fix this by adding a block name span for the block keyword, and special-case whether we are completing inside the name span. If we are, we don't continue to the following text node. PR Close #52198
This commit is contained in:
parent
54766fb35f
commit
e6affeff61
11 changed files with 1323 additions and 29 deletions
|
|
@ -89,8 +89,8 @@ export class Comment implements BaseNode {
|
|||
export class Block implements BaseNode {
|
||||
constructor(
|
||||
public name: string, public parameters: BlockParameter[], public children: Node[],
|
||||
public sourceSpan: ParseSourceSpan, public startSourceSpan: ParseSourceSpan,
|
||||
public endSourceSpan: ParseSourceSpan|null = null) {}
|
||||
public sourceSpan: ParseSourceSpan, public nameSpan: ParseSourceSpan,
|
||||
public startSourceSpan: ParseSourceSpan, public endSourceSpan: ParseSourceSpan|null = null) {}
|
||||
|
||||
visit(visitor: Visitor, context: any) {
|
||||
return visitor.visitBlock(this, context);
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ export class WhitespaceVisitor implements html.Visitor {
|
|||
visitBlock(block: html.Block, context: any): any {
|
||||
return new html.Block(
|
||||
block.name, block.parameters, visitAllWithSiblings(this, block.children), block.sourceSpan,
|
||||
block.startSourceSpan, block.endSourceSpan);
|
||||
block.nameSpan, block.startSourceSpan, block.endSourceSpan);
|
||||
}
|
||||
|
||||
visitBlockParameter(parameter: html.BlockParameter, context: any) {
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ class _Expander implements html.Visitor {
|
|||
visitBlock(block: html.Block, context: any) {
|
||||
return new html.Block(
|
||||
block.name, block.parameters, html.visitAll(this, block.children), block.sourceSpan,
|
||||
block.startSourceSpan, block.endSourceSpan);
|
||||
block.nameSpan, block.startSourceSpan, block.endSourceSpan);
|
||||
}
|
||||
|
||||
visitBlockParameter(parameter: html.BlockParameter, context: any) {
|
||||
|
|
|
|||
|
|
@ -918,7 +918,7 @@ class _Tokenizer {
|
|||
}
|
||||
|
||||
if (this._tokenizeBlocks && !this._inInterpolation && !this._isInExpansion() &&
|
||||
(this._isBlockStart() || this._cursor.peek() === chars.$RBRACE)) {
|
||||
(this._cursor.peek() === chars.$AT || this._cursor.peek() === chars.$RBRACE)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -944,19 +944,6 @@ class _Tokenizer {
|
|||
return false;
|
||||
}
|
||||
|
||||
private _isBlockStart(): boolean {
|
||||
if (this._tokenizeBlocks && this._cursor.peek() === chars.$AT) {
|
||||
const tmp = this._cursor.clone();
|
||||
|
||||
// If it is, also verify that the next character is a valid block identifier.
|
||||
tmp.advance();
|
||||
if (isBlockNameChar(tmp.peek())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _readUntil(char: number): string {
|
||||
const start = this._cursor.clone();
|
||||
this._attemptUntilChar(char);
|
||||
|
|
|
|||
|
|
@ -474,7 +474,7 @@ class _TreeBuilder {
|
|||
const span = new ParseSourceSpan(token.sourceSpan.start, end, token.sourceSpan.fullStart);
|
||||
// Create a separate `startSpan` because `span` will be modified when there is an `end` span.
|
||||
const startSpan = new ParseSourceSpan(token.sourceSpan.start, end, token.sourceSpan.fullStart);
|
||||
const block = new html.Block(token.parts[0], parameters, [], span, startSpan);
|
||||
const block = new html.Block(token.parts[0], parameters, [], span, token.sourceSpan, startSpan);
|
||||
this._pushContainer(block, false);
|
||||
}
|
||||
|
||||
|
|
@ -500,7 +500,7 @@ class _TreeBuilder {
|
|||
const span = new ParseSourceSpan(token.sourceSpan.start, end, token.sourceSpan.fullStart);
|
||||
// Create a separate `startSpan` because `span` will be modified when there is an `end` span.
|
||||
const startSpan = new ParseSourceSpan(token.sourceSpan.start, end, token.sourceSpan.fullStart);
|
||||
const block = new html.Block(token.parts[0], parameters, [], span, startSpan);
|
||||
const block = new html.Block(token.parts[0], parameters, [], span, token.sourceSpan, startSpan);
|
||||
this._pushContainer(block, false);
|
||||
|
||||
// Incomplete blocks don't have children so we close them immediately and report an error.
|
||||
|
|
|
|||
|
|
@ -315,7 +315,8 @@ export class IfBlockBranch implements Node {
|
|||
}
|
||||
|
||||
export class UnknownBlock implements Node {
|
||||
constructor(public name: string, public sourceSpan: ParseSourceSpan) {}
|
||||
constructor(
|
||||
public name: string, public sourceSpan: ParseSourceSpan, public nameSpan: ParseSourceSpan) {}
|
||||
|
||||
visit<Result>(visitor: Visitor<Result>): Result {
|
||||
return visitor.visitUnknownBlock(this);
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ export function createSwitchBlock(
|
|||
}
|
||||
|
||||
if ((node.name !== 'case' || node.parameters.length === 0) && node.name !== 'default') {
|
||||
unknownBlocks.push(new t.UnknownBlock(node.name, node.sourceSpan));
|
||||
unknownBlocks.push(new t.UnknownBlock(node.name, node.sourceSpan, node.nameSpan));
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -383,7 +383,7 @@ class HtmlAstToIvyAst implements html.Visitor {
|
|||
}
|
||||
|
||||
result = {
|
||||
node: new t.UnknownBlock(block.name, block.sourceSpan),
|
||||
node: new t.UnknownBlock(block.name, block.sourceSpan, block.nameSpan),
|
||||
errors: [new ParseError(block.sourceSpan, errorMessage)],
|
||||
};
|
||||
break;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -380,6 +380,14 @@ class TemplateTargetVisitor implements t.Visitor {
|
|||
// nodes.
|
||||
return;
|
||||
}
|
||||
if (last instanceof t.UnknownBlock && isWithin(this.position, last.nameSpan)) {
|
||||
// Autocompletions such as `@\nfoo`, where a newline follows a bare `@`, would not work
|
||||
// because the language service visitor sees us inside the subsequent text node. We deal with
|
||||
// this with using a special-case: if we are completing inside the name span, we don't
|
||||
// continue to the subsequent text node.
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTemplateNodeWithKeyAndValue(node) && !isWithinKeyValue(this.position, node)) {
|
||||
// If cursor is within source span but not within key span or value span,
|
||||
// do not return the node.
|
||||
|
|
|
|||
|
|
@ -282,12 +282,64 @@ describe('completions', () => {
|
|||
});
|
||||
|
||||
describe('for blocks', () => {
|
||||
it('at top level', () => {
|
||||
const {templateFile} = setup(`@`, ``);
|
||||
templateFile.moveCursorToText('@¦');
|
||||
const completions = templateFile.getCompletionsAtPosition();
|
||||
expectContain(
|
||||
completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.BLOCK), ['if']);
|
||||
const completionPrefixes = ['@', '@i'];
|
||||
|
||||
describe(`at top level`, () => {
|
||||
for (const completionPrefix of completionPrefixes) {
|
||||
it(`in empty file (with prefix ${completionPrefix})`, () => {
|
||||
const {templateFile} = setup(`${completionPrefix}`, ``);
|
||||
templateFile.moveCursorToText(`${completionPrefix}¦`);
|
||||
const completions = templateFile.getCompletionsAtPosition();
|
||||
expectContain(
|
||||
completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.BLOCK),
|
||||
['if']);
|
||||
});
|
||||
|
||||
it(`after text (with prefix ${completionPrefix})`, () => {
|
||||
const {templateFile} = setup(`foo ${completionPrefix}`, ``);
|
||||
templateFile.moveCursorToText(`${completionPrefix}¦`);
|
||||
const completions = templateFile.getCompletionsAtPosition();
|
||||
expectContain(
|
||||
completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.BLOCK),
|
||||
['if']);
|
||||
});
|
||||
|
||||
it(`before text (with prefix ${completionPrefix})`, () => {
|
||||
const {templateFile} = setup(`${completionPrefix} foo`, ``);
|
||||
templateFile.moveCursorToText(`${completionPrefix}¦`);
|
||||
const completions = templateFile.getCompletionsAtPosition();
|
||||
expectContain(
|
||||
completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.BLOCK),
|
||||
['if']);
|
||||
});
|
||||
|
||||
it(`after newline with text on preceding line (with prefix ${completionPrefix})`, () => {
|
||||
const {templateFile} = setup(`foo\n${completionPrefix}`, ``);
|
||||
templateFile.moveCursorToText(`${completionPrefix}¦`);
|
||||
const completions = templateFile.getCompletionsAtPosition();
|
||||
expectContain(
|
||||
completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.BLOCK),
|
||||
['if']);
|
||||
});
|
||||
|
||||
it(`before newline with text on newline (with prefix ${completionPrefix})`, () => {
|
||||
const {templateFile} = setup(`${completionPrefix}\nfoo`, ``);
|
||||
templateFile.moveCursorToText(`${completionPrefix}¦`);
|
||||
const completions = templateFile.getCompletionsAtPosition();
|
||||
expectContain(
|
||||
completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.BLOCK),
|
||||
['if']);
|
||||
});
|
||||
|
||||
it(`in a practical case, on its own line (with prefix ${completionPrefix})`, () => {
|
||||
const {templateFile} = setup(`<div></div>\n ${completionPrefix}\n<span></span>`, ``);
|
||||
templateFile.moveCursorToText(`${completionPrefix}¦`);
|
||||
const completions = templateFile.getCompletionsAtPosition();
|
||||
expectContain(
|
||||
completions, unsafeCastDisplayInfoKindToScriptElementKind(DisplayInfoKind.BLOCK),
|
||||
['if']);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('inside if', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue