10000 refactor(language-service): set up template targets for selectorless … · angular/angular@c69dda6 · GitHub
[go: up one dir, main page]

Skip to content

Commit c69dda6

Browse files
crisbetoalxhub
authored andcommitted
refactor(language-service): set up template targets for selectorless (#61240)
Adds the logic to resolve the template targets for the selectorless component and directive nodes. This is a prerequisite for other functionality. PR Close #61240
1 parent 109e49c commit c69dda6

File tree

2 files changed

+149
-40
lines changed

2 files changed

+149
-40
lines changed

packages/language-service/src/template_target.ts

Lines changed: 107 additions & 39 deletions
< 341A td data-grid-cell-id="diff-ec57a8e261b398b22d55d2e3acae12aaa615432863a8c138168391a0ea8da575-179-223-0" data-selected="false" role="gridcell" style="background-color:var(--diffBlob-additionNum-bgColor, var(--diffBlob-addition-bgColor-num));text-align:center" tabindex="-1" valign="top" class="focusable-grid-cell diff-line-number position-relative left-side">
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,11 @@ export type SingleNodeTarget =
105105
| ElementInBodyContext
106106
| ElementInTagContext
107107
| AttributeInKeyContext
108-
| AttributeInValueContext;
108+
| AttributeInValueContext
109+
| ComponentInBodyContext
110+
| ComponentInTagContext
111+
| DirectiveInNameContext
112+
| DirectiveInBodyContext;
109113

110114
/**
111115
* Contexts which logically target multiple nodes in the template AST, which cannot be
@@ -127,6 +131,10 @@ export enum TargetNodeKind {
127131
AttributeInKeyContext,
128132
AttributeInValueContext,
129133
TwoWayBindingContext,
134+
ComponentInTagContext,
135+
ComponentInBodyContext,
136+
DirectiveInNameContext,
137+
DirectiveInBodyContext,
130138
}
131139

132140
/**
@@ -177,6 +185,42 @@ export interface ElementInBodyContext {
177185
node: TmplAstElement | TmplAstTemplate;
178186
}
179187

188+
/**
189+
* A `TmplAstComponent` element node that's targeted, where the given position is within the tag,
190+
* e.g. `MyComp` in `<MyComp foo="bar"/>`.
191+
*/
192+
export interface ComponentInTagContext {
193+
kind: TargetNodeKind.ComponentInTagContext;
194+
node: TmplAstComponent;
195+
}
196+
197+
/**
198+
* A `TmplAstComponent` element node that's targeted, where the given position is within the body,
199+
* e.g. `foo="bar"/>` in `<MyComp foo="bar"/>`.
200+
*/
201+
export interface ComponentInBodyContext {
202+
kind: TargetNodeKind.ComponentInBodyContext;
203+
node: TmplAstComponent;
204+
}
205+
206+
/**
207+
* A `TmplAstDirective` element node that's targeted, where the given position is within the
208+
* directive's name (e.g. `MyDir` in `@MyDir`).
209+
*/
210+
export interface DirectiveInNameContext {
211+
kind: TargetNodeKind.DirectiveInNameContext;
212+
node: TmplAstDirective;
213+
}
214+
215+
/**
216+
* A `TmplAstDirective` element node that's targeted, where the given position is within the body,
217+
* e.g. `(foo="bar")` in `@MyDir(foo="bar")`.
218+
*/
219+
export interface DirectiveInBodyContext {
220+
kind: TargetNodeKind.DirectiveInBodyContext;
221+
node: TmplAstDirective;
222+
}
223+
180224
export interface AttributeInKeyContext {
181225
kind: TargetNodeKind.AttributeInKeyContext;
182226
node: TmplAstTextAttribute | TmplAstBoundAttribute | TmplAstBoundEvent;
@@ -267,22 +311,32 @@ export function getTargetAtPosition(
267311
} else if (candidate instanceof TmplAstElement) {
268312
// Elements have two contexts: the tag context (position is within the element tag) or the
269313
// element body context (position is outside of the tag name, but still in the element).
314+
nodeInContext = {
315+
kind: isWithinTagBody(position, candidate)
316+
? TargetNodeKind.ElementInBodyContext
317+
: TargetNodeKind.ElementInTagContext,
318+
node: candidate,
319+
};
320+
} else if (candidate instanceof TmplAstComponent) {
321+
nodeInContext = {
322+
kind: isWithinTagBody(position, candidate)
323+
? TargetNodeKind.ComponentInBodyContext
324+
: TargetNodeKind.ComponentInTagContext,
325+
node: candidate,
326+
};
327+
} else if (candidate instanceof TmplAstDirective) {
328+
const startSpan = candidate.startSourceSpan;
270329

271-
// Calculate the end of the element tag name. Any position beyond this is in the element body.
272-
const tagEndPos =
273-
candidate.sourceSpan.start.offset + 1 /* '<' element open */ + candidate.name.length;
274-
if (position > tagEndPos) {
275-
// Position is within the element body
276-
nodeInContext = {
277-
kind: TargetNodeKind.ElementInBodyContext,
278-
node: candidate,
279-
};
280-
} else {
281-
nodeInContext = {
282-
kind: TargetNodeKind.ElementInTagContext,
283-
node: candidate,
284-
};
285-
}
330+
// The start span includes the opening paren, if there is one which we have to account for.
331+
const endOffset = startSpan.end.offset - (startSpan.toString().endsWith('(') ? 1 : 0);
332+
333+
nodeInContext = {
334+
kind:
335+
position >= startSpan.start.offset && position <= endOffset
336+
? TargetNodeKind.DirectiveInNameContext
337+
: TargetNodeKind.DirectiveInBodyContext,
338+
node: candidate,
339+
};
286340
} else if (
287341
(candidate instanceof TmplAstBoundAttribute ||
288342
candidate instanceof TmplAstBoundEvent ||
@@ -474,48 +528,61 @@ class TemplateTargetVisitor implements TmplAstVisitor {
474528
}
475529

476530
visitElement(element: TmplAstElement) {
477-
this.visitElementOrTemplate(element);
531+
this.visitDirectiveHost(element);
478532
}
479533

480534
visitTemplate(template: TmplAstTemplate) {
481-
this.visitElementOrTemplate(template);
535+
this.visitDirectiveHost(template);
536+
}
537+
538+
visitComponent(component: TmplAstComponent) {
539+
this.visitDirectiveHost(component);
482540
}
483541

484-
visitElementOrTemplate(element: TmplAstTemplate | TmplAstElement) {
485-
const isTemplate = element instanceof TmplAstTemplate;
486-
this.visitAll(element.attributes);
487-
if (!isTemplate) {
488-
this.visitAll(element.directives);
542+
visitDirective(directive: TmplAstDirective) {
543+
this.visitDirectiveHost(directive);
544+
}
545+
546+
private visitDirectiveHost(
547+
node: TmplAstTemplate | TmplAstElement | TmplAstComponent | TmplAstDirective,
548+
) {
549+
const isTemplate = node instanceof TmplAstTemplate;
550+
const isDirective = node instanceof TmplAstDirective;
551+
this.visitAll(node.attributes);
552+
if (!isDirective) {
553+
this.visitAll(node.directives);
489554
}
490-
this.visitAll(element.inputs);
555+
this.visitAll(node.inputs);
491556
// We allow the path to contain both the `TmplAstBoundAttribute` and `TmplAstBoundEvent` for
492557
// two-way bindings but do not want the path to contain both the `TmplAstBoundAttribute` with
493558
// its children when the position is in the value span because we would then logically create a
494559
// path that also contains the `PropertyWrite` from the `TmplAstBoundEvent`. This early return
495560
// condition ensures we target just `TmplAstBoundAttribute` for this case and exclude
496561
// `TmplAstBoundEvent` children.
497562
if (
498-
this.path[this.path.length - 1] !== element &&
563+
this.path[this.path.length - 1] !== node &&
499564
!(this.path[this.path.length - 1] instanceof TmplAstBoundAttribute)
500565
) {
501566
return;
502567
}
503-
this.visitAll(element.outputs);
568+
this.visitAll(node.outputs);
504569
if (isTemplate) {
505-
this.visitAll(element.templateAttrs);
570+
this.visitAll(node.templateAttrs);
506571
}
507-
this.visitAll(element.references);
572+
this.visitAll(node.references);
508573
if (isTemplate) {
509-
this.visitAll(element.variables);
574+
this.visitAll(node.variables);
510575
}
511576

512577
// If we get here and have not found a candidate node on the element itself, proceed with
513578
// looking for a more specific node on the element children.
514-
if (this.path[this.path.length - 1] !== element) {
579+
if (this.path[this.path.length - 1] !== node) {
515580
return;
516581
}
517582

518-
this.visitAll(element.children);
583+
if (!isDirective) {
584+
this.visitAll(node.children);
585+
}
519586
}
520587

521588
visitContent(content: TmplAstContent) {
@@ -626,14 +693,6 @@ class TemplateTargetVisitor implements TmplAstVisitor {
626693
this.visitBinding(decl.value);
627694
}
628695

629-
visitComponent(component: TmplAstComponent) {
630-
// TODO(crisbeto): integrate selectorless
631-
}
632-
633-
visitDirective(directive: TmplAstDirective) {
634-
// TODO(crisbeto): integrate selectorless
635-
}
636-
637696
visitAll(nodes: TmplAstNode[]) {
638697
for (const node of nodes) {
639698
this.visit(node);
@@ -708,3 +767,12 @@ function isWithinNode(position: number, node: TmplAstNode): boolean {
708767
node.listeners.some((listener) => isWithin(position, listener.sourceSpan)))
709768
);
710769
}
770+
771+
/** Checks whether a position is within the body or the start syntax of a node. */
772+
function isWithinTagBody(position: number, node: TmplAstElement | TmplAstComponent): boolean {
773+
// Calculate the end of the element tag name. Any position beyond this is in the body.
774+
const name = node instanceof TmplAstComponent ? node.fullName : node.name;
775+
const tagEndPos =
776+
node.sourceSpan.start.offset + 1 /* '<' is the opening character */ + name.length;
777+
return position > tagEndPos;
778+
}

packages/language-service/test/legacy/template_target_spec.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ interface ParseResult {
2424
position: number;
2525
}
2626

27-
function parse(template: string): ParseResult {
27+
function parse(template: string, enableSelectorless = false): ParseResult {
2828
const position = template.indexOf('¦');
2929
if (position < 0) {
3030
throw new Error(`Template "${template}" does not contain the cursor`);
@@ -41,6 +41,7 @@ function parse(template: string): ParseResult {
4141
leadingTriviaChars: [],
4242
preserveWhitespaces: true,
4343
alwaysAttemptHtmlToR3AstConversion: true,
44+
enableSelectorless,
4445
}),
4546
position,
4647
};
@@ -896,6 +897,46 @@ describe('findNodeAtPosition for microsyntax expression', () => {
896897
expect(context.kind).toBe(TargetNodeKind.ElementInBodyContext);
897898
expect((context as SingleNodeTarget).node).toBeInstanceOf(t.Element);
898899
});
900+
901+
it('should locate a component in its tag context', () => {
902+
const {errors, nodes, position} = parse(`<Comp¦ attr/>`, true);
903+
expect(errors).toBe(null);
904+
const {context} = getTargetAtPosition(nodes, position)!;
905+
expect(context.kind).toBe(TargetNodeKind.ComponentInTagContext);
906+
expect((context as SingleNodeTarget).node).toBeInstanceOf(t.Component);
907+
});
908+
909+
it('should locate a component in its body context', () => {
910+
const {errors, nodes, position} = parse(`<Comp ¦ attr/>`, true);
911+
expect(errors).toBe(null);
912+
const {context} = getTargetAtPosition(nodes, position)!;
913+
expect(context.kind).toBe(TargetNodeKind.ComponentInBodyContext);
914+
expect((context as SingleNodeTarget).node).toBeInstanceOf(t.Component);
915+
});
916+
917+
it('should locate a directive in its name context', () => {
918+
const {errors, nodes, position} = parse(`<div @Dir¦></div>`, true);
919+
expect(errors).toBe(null);
920+
const {context} = getTargetAtPosition(nodes, position)!;
921+
expect(context.kind).toBe(TargetNodeKind.DirectiveInNameContext);
922+
expect((context as SingleNodeTarget).node).toBeInstanceOf(t.Directive);
923+
});
924+
925+
it('should locate a directive in its body context when there are bindings', () => {
926+
const {errors, nodes, position} = parse(`<div @Dir([foo]="bar" ¦)></div>`, true);
927+
expect(errors).toBe(null);
928+
const {context} = getTargetAtPosition(nodes, position)!;
929+
expect(context.kind).toBe(TargetNodeKind.DirectiveInBodyContext);
930+
expect((context as SingleNodeTarget).node).toBeInstanceOf(t.Directive);
931+
});
932+
933+
it('should locate a directive in its body context when there are no bindings', () => {
934+
const {errors, nodes, position} = parse(`<div @Dir(¦)></div>`, true);
935+
expect(errors).toBe(null);
936+
const {context} = getTargetAtPosition(nodes, position)!;
937+
expect(context.kind).toBe(TargetNodeKind.DirectiveInBodyContext);
938+
expect((context as SingleNodeTarget).node).toBeInstanceOf(t.Directive);
939+
});
899940
});
900941

901942
describe('unclosed elements', () => {

0 commit comments

Comments
 (0)
0