angular/packages/language-service/test/get_outlining_spans_spec.ts
Andrew Scott 023a181ba5 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 (https://github.com/angular/vscode-ng-language-service/issues/1930)

PR Close #52062
2023-10-09 10:20:26 -07:00

207 lines
6.6 KiB
TypeScript

/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {initMockFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing';
import ts from 'typescript';
import {createModuleAndProjectWithDeclarations, LanguageServiceTestEnv} from '../testing';
describe('get outlining spans', () => {
beforeEach(() => {
initMockFileSystem('Native');
});
it('should get block outlining spans for an inline template', () => {
const files = {
'app.ts': `
import {Component} from '@angular/core';
@Component({
template: \`
@if (1) { if body }
\`,
})
export class AppCmp {
}`
};
const env = LanguageServiceTestEnv.setup();
const project = createModuleAndProjectWithDeclarations(env, 'test', files);
project.expectNoSourceDiagnostics();
const appFile = project.openFile('app.ts');
const result = appFile.getOutliningSpans();
const {textSpan} = result[0];
expect(files['app.ts'].substring(textSpan.start, textSpan.start + textSpan.length))
.toEqual(' if body ');
});
it('should get block outlining spans for an external template', () => {
const files = {
'app.ts': `
import {Component} from '@angular/core';
@Component({
templateUrl: './app.html',
})
export class AppCmp {
}`,
'app.html': '@defer { lazy text }'
};
const env = LanguageServiceTestEnv.setup();
const project = createModuleAndProjectWithDeclarations(env, 'test', files);
project.expectNoSourceDiagnostics();
const appFile = project.openFile('app.html');
const result = appFile.getOutliningSpans();
const {textSpan} = result[0];
expect(files['app.html'].substring(textSpan.start, textSpan.start + textSpan.length))
.toEqual(' lazy text ');
});
it('should have outlining spans for all defer block parts', () => {
const files = {
'app.ts': `
import {Component} from '@angular/core';
@Component({
template: \`
@defer {
defer main block
} @placeholder {
defer placeholder block
} @error {
defer error block
} @loading {
defer loading block
}
\`
})
export class AppCmp {
}`,
};
const env = LanguageServiceTestEnv.setup();
const project = createModuleAndProjectWithDeclarations(env, 'test', files);
project.expectNoSourceDiagnostics();
const appFile = project.openFile('app.ts');
const result = appFile.getOutliningSpans();
expect(getTrimmedSpanText(result[0].textSpan, files['app.ts'])).toEqual('defer main block');
expect(getTrimmedSpanText(result[1].textSpan, files['app.ts']))
.toEqual('defer placeholder block');
expect(getTrimmedSpanText(result[2].textSpan, files['app.ts'])).toEqual('defer loading block');
expect(getTrimmedSpanText(result[3].textSpan, files['app.ts'])).toEqual('defer error block');
});
it('should have outlining spans for all connected if blocks', () => {
const files = {
'app.ts': `
import {Component} from '@angular/core';
@Component({
template: \`
@if (1) {
if1
} @else if (2) {
elseif2
} @else if (3) {
elseif3
} @else {
else block
}
\`
})
export class AppCmp {
}`,
};
const env = LanguageServiceTestEnv.setup();
const project = createModuleAndProjectWithDeclarations(env, 'test', files);
project.expectNoSourceDiagnostics();
const appFile = project.openFile('app.ts');
const result = appFile.getOutliningSpans();
expect(getTrimmedSpanText(result[0].textSpan, files['app.ts'])).toEqual('if1');
expect(getTrimmedSpanText(result[1].textSpan, files['app.ts'])).toEqual('elseif2');
expect(getTrimmedSpanText(result[2].textSpan, files['app.ts'])).toEqual('elseif3');
expect(getTrimmedSpanText(result[3].textSpan, files['app.ts'])).toEqual('else block');
});
it('should have outlining spans for all switch cases, including the main', () => {
const files = {
'app.ts': `
import {Component} from '@angular/core';
@Component({
template: \`
@switch (test) {
@case ('test') {
yes
} @case ('x') {
definitely not
} @case ('y') {
stop trying
} @default {
just in case
}
}
\`
})
export class AppCmp {
test = 'test';
}`,
};
const env = LanguageServiceTestEnv.setup();
const project = createModuleAndProjectWithDeclarations(env, 'test', files);
project.expectNoSourceDiagnostics();
const appFile = project.openFile('app.ts');
const result = appFile.getOutliningSpans();
expect(getTrimmedSpanText(result[0].textSpan, files['app.ts']))
.toMatch(/case..'test'.*default.*\}/);
expect(getTrimmedSpanText(result[1].textSpan, files['app.ts'])).toEqual('yes');
expect(getTrimmedSpanText(result[2].textSpan, files['app.ts'])).toEqual('definitely not');
expect(getTrimmedSpanText(result[3].textSpan, files['app.ts'])).toEqual('stop trying');
expect(getTrimmedSpanText(result[4].textSpan, files['app.ts'])).toEqual('just in case');
});
it('should have outlining spans for repeater and empty block', () => {
const files = {
'app.ts': `
import {Component} from '@angular/core';
@Component({
template: \`
@for (item of items; track $index) {
{{item}}
} @empty {
empty list
}
\`
})
export class AppCmp {
items = [];
}`,
};
const env = LanguageServiceTestEnv.setup();
const project = createModuleAndProjectWithDeclarations(env, 'test', files);
project.expectNoSourceDiagnostics();
const appFile = project.openFile('app.ts');
const result = appFile.getOutliningSpans();
expect(getTrimmedSpanText(result[0].textSpan, files['app.ts'])).toEqual('{{item}}');
expect(getTrimmedSpanText(result[1].textSpan, files['app.ts'])).toEqual('empty list');
});
});
function getTrimmedSpanText(span: ts.TextSpan, contents: string) {
return trim(contents.substring(span.start, span.start + span.length));
}
function trim(text: string|null): string {
return text ? text.replace(/[\s\n]+/gm, ' ').trim() : '';
}