Skip to content

Commit 023a181

Browse files
committed
feat(language-service): Implement outlining spans for control flow blocks (#52062)
This commit implements the getOutlingSpans to retrieve Angular-specific outlining spans. At the moment, these spans are limited to control-flow blocks in templates. This is required for folding ranges (angular/vscode-ng-language-service#1930) PR Close #52062
1 parent 28a5925 commit 023a181

File tree

9 files changed

+334
-17
lines changed

9 files changed

+334
-17
lines changed

packages/compiler-cli/src/ngtsc/perf/src/api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,11 @@ export enum PerfPhase {
149149
*/
150150
LsSignatureHelp,
151151

152+
/**
153+
* Time spent by the Angular Language Service calculating outlining spans.
154+
*/
155+
OutliningSpans,
156+
152157
/**
153158
* Tracks the number of `PerfPhase`s, and must appear at the end of the list.
154159
*/

packages/compiler/src/render3/r3_ast.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ export class SwitchBlock implements Node {
252252
export class SwitchBlockCase implements Node {
253253
constructor(
254254
public expression: AST|null, public children: Node[], public sourceSpan: ParseSourceSpan,
255-
public startSourceSpan: ParseSourceSpan) {}
255+
public startSourceSpan: ParseSourceSpan, public endSourceSpan: ParseSourceSpan|null) {}
256256

257257
visit<Result>(visitor: Visitor<Result>): Result {
258258
return visitor.visitSwitchBlockCase(this);
@@ -445,10 +445,9 @@ export class RecursiveVisitor implements Visitor<void> {
445445
visitAll(this, block.children);
446446
}
447447
visitForLoopBlock(block: ForLoopBlock): void {
448-
block.item.visit(this);
449-
visitAll(this, Object.values(block.contextVariables));
450-
visitAll(this, block.children);
451-
block.empty?.visit(this);
448+
const blockItems = [block.item, ...Object.values(block.contextVariables), ...block.children];
449+
block.empty && blockItems.push(block.empty);
450+
visitAll(this, blockItems);
452451
}
453452
visitForLoopBlockEmpty(block: ForLoopBlockEmpty): void {
454453
visitAll(this, block.children);
@@ -457,8 +456,9 @@ export class RecursiveVisitor implements Visitor<void> {
457456
visitAll(this, block.branches);
458457
}
459458
visitIfBlockBranch(block: IfBlockBranch): void {
460-
visitAll(this, block.children);
461-
block.expressionAlias?.visit(this);
459+
const blockItems = block.children;
460+
block.expressionAlias && blockItems.push(block.expressionAlias);
461+
visitAll(this, blockItems);
462462
}
463463
visitContent(content: Content): void {}
464464
visitVariable(variable: Variable): void {}

packages/compiler/src/render3/r3_control_flow.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ export function createSwitchBlock(
175175
null;
176176
const ast = new t.SwitchBlockCase(
177177
expression, html.visitAll(visitor, node.children, node.children), node.sourceSpan,
178-
node.startSourceSpan);
178+
node.startSourceSpan, node.endSourceSpan);
179179

180180
if (expression === null) {
181181
defaultCase = ast;

packages/language-service/src/language_service.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,30 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {AbsoluteSourceSpan, AST, ParseSourceSpan, TmplAstBoundEvent, TmplAstNode} from '@angular/compiler';
9+
import {AST, TmplAstNode} from '@angular/compiler';
1010
import {CompilerOptions, ConfigurationHost, readConfiguration} from '@angular/compiler-cli';
1111
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
1212
import {ErrorCode, ngErrorCode} from '@angular/compiler-cli/src/ngtsc/diagnostics';
13-
import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
13+
import {absoluteFrom, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
1414
import {PerfPhase} from '@angular/compiler-cli/src/ngtsc/perf';
1515
import {FileUpdate, ProgramDriver} from '@angular/compiler-cli/src/ngtsc/program_driver';
1616
import {isNamedClassDeclaration} from '@angular/compiler-cli/src/ngtsc/reflection';
17-
import {TypeCheckShimGenerator} from '@angular/compiler-cli/src/ngtsc/typecheck';
1817
import {OptimizeFor} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
19-
import {findFirstMatchingNode} from '@angular/compiler-cli/src/ngtsc/typecheck/src/comments';
2018
import ts from 'typescript/lib/tsserverlibrary';
2119

2220
import {GetComponentLocationsForTemplateResponse, GetTcbResponse, GetTemplateLocationForComponentResponse} from '../api';
2321

2422
import {LanguageServiceAdapter, LSParseConfigHost} from './adapters';
2523
import {ALL_CODE_FIXES_METAS, CodeFixes} from './codefixes';
2624
import {CompilerFactory} from './compiler_factory';
27-
import {CompletionBuilder, CompletionNodeContext} from './completions';
25+
import {CompletionBuilder} from './completions';
2826
import {DefinitionBuilder} from './definitions';
27+
import {getOutliningSpans} from './outlining_spans';
2928
import {QuickInfoBuilder} from './quick_info';
3029
import {ReferencesBuilder, RenameBuilder} from './references_and_rename';
3130
import {createLocationKey} from './references_and_rename_utils';
3231
import {getSignatureHelp} from './signature_help';
33-
import {getTargetAtPosition, getTcbNodesOfTemplateAtPosition, TargetContext, TargetNodeKind} from './template_target';
32+
import {getTargetAtPosition, getTcbNodesOfTemplateAtPosition, TargetNodeKind} from './template_target';
3433
import {findTightestNode, getClassDeclFromDecoratorProp, getParentClassDeclaration, getPropertyAssignmentFromValue} from './ts_utils';
3534
import {getTemplateInfoAtPosition, isTypeScriptFile} from './utils';
3635

@@ -271,8 +270,12 @@ export class LanguageService {
271270
}
272271

273272
return getSignatureHelp(compiler, this.tsLS, fileName, position, options);
273+
});
274+
}
274275

275-
return undefined;
276+
getOutliningSpans(fileName: string): ts.OutliningSpan[] {
277+
return this.withCompilerAndPerfTracing(PerfPhase.OutliningSpans, compiler => {
278+
return getOutliningSpans(compiler, fileName);
276279
});
277280
}
278281

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {ParseLocation, ParseSourceSpan} from '@angular/compiler';
10+
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
11+
import {isExternalResource} from '@angular/compiler-cli/src/ngtsc/metadata';
12+
import {isNamedClassDeclaration} from '@angular/compiler-cli/src/ngtsc/reflection';
13+
import * as t from '@angular/compiler/src/render3/r3_ast'; // t for template AST
14+
import ts from 'typescript';
15+
16+
import {getFirstComponentForTemplateFile, isTypeScriptFile, toTextSpan} from './utils';
17+
18+
export function getOutliningSpans(compiler: NgCompiler, fileName: string): ts.OutliningSpan[] {
19+
if (isTypeScriptFile(fileName)) {
20+
const sf = compiler.getCurrentProgram().getSourceFile(fileName);
21+
if (sf === undefined) {
22+
return [];
23+
}
24+
25+
const templatesInFile: Array<t.Node[]> = [];
26+
for (const stmt of sf.statements) {
27+
if (isNamedClassDeclaration(stmt)) {
28+
const resources = compiler.getComponentResources(stmt);
29+
if (resources === null || isExternalResource(resources.template)) {
30+
continue;
31+
}
32+
const template = compiler.getTemplateTypeChecker().getTemplate(stmt);
33+
if (template === null) {
34+
continue;
35+
}
36+
templatesInFile.push(template);
37+
}
38+
}
39+
return templatesInFile.map(template => BlockVisitor.getBlockSpans(template)).flat();
40+
} else {
41+
const templateInfo = getFirstComponentForTemplateFile(fileName, compiler);
42+
if (templateInfo === undefined) {
43+
return [];
44+
}
45+
const {template} = templateInfo;
46+
return BlockVisitor.getBlockSpans(template);
47+
}
48+
}
49+
50+
class BlockVisitor extends t.RecursiveVisitor {
51+
readonly blocks = [] as
52+
Array<t.IfBlockBranch|t.ForLoopBlockEmpty|t.ForLoopBlock|t.SwitchBlockCase|t.SwitchBlock|
53+
t.DeferredBlockError|t.DeferredBlockPlaceholder|t.DeferredBlockLoading>;
54+
55+
static getBlockSpans(templateNodes: t.Node[]): ts.OutliningSpan[] {
56+
const visitor = new BlockVisitor();
57+
t.visitAll(visitor, templateNodes);
58+
const {blocks} = visitor;
59+
return blocks.map(block => {
60+
let mainBlockSpan = block.sourceSpan;
61+
// The source span of for loops and deferred blocks contain all parts (ForLoopBlockEmpty,
62+
// DeferredBlockLoading, etc.). The folding range should only include the main block span for
63+
// these.
64+
if (block instanceof t.ForLoopBlock || block instanceof t.DeferredBlock) {
65+
mainBlockSpan = block.mainBlockSpan;
66+
}
67+
return {
68+
// We move the end back 1 character so we do not consume the close brace of the block in the
69+
// range.
70+
textSpan: toTextSpan(
71+
new ParseSourceSpan(block.startSourceSpan.end, mainBlockSpan.end.moveBy(-1))),
72+
hintSpan: toTextSpan(block.startSourceSpan),
73+
bannerText: '...',
74+
autoCollapse: false,
75+
kind: ts.OutliningSpanKind.Region,
76+
};
77+
});
78+
}
79+
80+
visit(node: t.Node) {
81+
if (node instanceof t.IfBlockBranch || node instanceof t.ForLoopBlockEmpty ||
82+
node instanceof t.ForLoopBlock || node instanceof t.SwitchBlockCase ||
83+
node instanceof t.SwitchBlock || node instanceof t.DeferredBlockError ||
84+
node instanceof t.DeferredBlockPlaceholder || node instanceof t.DeferredBlockLoading ||
85+
node instanceof t.DeferredBlock) {
86+
this.blocks.push(node);
87+
}
88+
}
89+
}

packages/language-service/src/ts_plugin.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,14 @@ export function create(info: ts.server.PluginCreateInfo): NgLanguageService {
145145
}
146146
}
147147

148+
function getOutliningSpans(fileName: string): ts.OutliningSpan[] {
149+
if (angularOnly) {
150+
return ngLS.getOutliningSpans(fileName);
151+
} else {
152+
return tsLS.getOutliningSpans(fileName) ?? ngLS.getOutliningSpans(fileName);
153+
}
154+
}
155+
148156
function getTcb(fileName: string, position: number): GetTcbResponse|undefined {
149157
return ngLS.getTcb(fileName, position);
150158
}
@@ -217,6 +225,7 @@ export function create(info: ts.server.PluginCreateInfo): NgLanguageService {
217225
getCompilerOptionsDiagnostics,
218226
getComponentLocationsForTemplate,
219227
getSignatureHelpItems,
228+
getOutliningSpans,
220229
getTemplateLocationForComponent,
221230
getCodeFixesAtPosition,
222231
getCombinedCodeFix,

packages/language-service/src/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,8 @@ function tsDeclarationSortComparator(a: DeclarationNode, b: DeclarationNode): nu
147147
}
148148
}
149149

150-
function getFirstComponentForTemplateFile(fileName: string, compiler: NgCompiler): TemplateInfo|
151-
undefined {
150+
export function getFirstComponentForTemplateFile(
151+
fileName: string, compiler: NgCompiler): TemplateInfo|undefined {
152152
const templateTypeChecker = compiler.getTemplateTypeChecker();
153153
const components = compiler.getComponentsWithTemplateFile(fileName);
154154
const sortedComponents = Array.from(components).sort(tsDeclarationSortComparator);

0 commit comments

Comments
 (0)