feat(language-service): support TS2.2 plugin model
This commit is contained in:
parent
e5c6bb4286
commit
99aa49ab6c
|
@ -84,7 +84,7 @@ export class StaticReflector implements ReflectorReader {
|
||||||
const classMetadata = this.getTypeMetadata(type);
|
const classMetadata = this.getTypeMetadata(type);
|
||||||
if (classMetadata['extends']) {
|
if (classMetadata['extends']) {
|
||||||
const parentType = this.simplify(type, classMetadata['extends']);
|
const parentType = this.simplify(type, classMetadata['extends']);
|
||||||
if (parentType instanceof StaticSymbol) {
|
if (parentType && (parentType instanceof StaticSymbol)) {
|
||||||
const parentAnnotations = this.annotations(parentType);
|
const parentAnnotations = this.annotations(parentType);
|
||||||
annotations.push(...parentAnnotations);
|
annotations.push(...parentAnnotations);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,11 +11,8 @@
|
||||||
* @description
|
* @description
|
||||||
* Entry point for all public APIs of the language service package.
|
* Entry point for all public APIs of the language service package.
|
||||||
*/
|
*/
|
||||||
import {LanguageServicePlugin} from './src/ts_plugin';
|
|
||||||
|
|
||||||
export {createLanguageService} from './src/language_service';
|
export {createLanguageService} from './src/language_service';
|
||||||
|
export {create} from './src/ts_plugin';
|
||||||
export {Completion, Completions, Declaration, Declarations, Definition, Diagnostic, Diagnostics, Hover, HoverTextSection, LanguageService, LanguageServiceHost, Location, Span, TemplateSource, TemplateSources} from './src/types';
|
export {Completion, Completions, Declaration, Declarations, Definition, Diagnostic, Diagnostics, Hover, HoverTextSection, LanguageService, LanguageServiceHost, Location, Span, TemplateSource, TemplateSources} from './src/types';
|
||||||
export {TypeScriptServiceHost, createLanguageServiceFromTypescript} from './src/typescript_host';
|
export {TypeScriptServiceHost, createLanguageServiceFromTypescript} from './src/typescript_host';
|
||||||
export {VERSION} from './src/version';
|
export {VERSION} from './src/version';
|
||||||
|
|
||||||
export default LanguageServicePlugin;
|
|
||||||
|
|
|
@ -107,8 +107,9 @@ class LanguageServiceImpl implements LanguageService {
|
||||||
getTemplateAst(template: TemplateSource, contextFile: string): AstResult {
|
getTemplateAst(template: TemplateSource, contextFile: string): AstResult {
|
||||||
let result: AstResult;
|
let result: AstResult;
|
||||||
try {
|
try {
|
||||||
const {metadata} =
|
const resolvedMetadata =
|
||||||
this.metadataResolver.getNonNormalizedDirectiveMetadata(template.type as any);
|
this.metadataResolver.getNonNormalizedDirectiveMetadata(template.type as any);
|
||||||
|
const metadata = resolvedMetadata && resolvedMetadata.metadata;
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
const rawHtmlParser = new HtmlParser();
|
const rawHtmlParser = new HtmlParser();
|
||||||
const htmlParser = new I18NHtmlParser(rawHtmlParser);
|
const htmlParser = new I18NHtmlParser(rawHtmlParser);
|
||||||
|
@ -124,9 +125,10 @@ class LanguageServiceImpl implements LanguageService {
|
||||||
ngModule = findSuitableDefaultModule(analyzedModules);
|
ngModule = findSuitableDefaultModule(analyzedModules);
|
||||||
}
|
}
|
||||||
if (ngModule) {
|
if (ngModule) {
|
||||||
const directives = ngModule.transitiveModule.directives.map(
|
const resolvedDirectives = ngModule.transitiveModule.directives.map(
|
||||||
d => this.host.resolver.getNonNormalizedDirectiveMetadata(d.reference)
|
d => this.host.resolver.getNonNormalizedDirectiveMetadata(d.reference));
|
||||||
.metadata.toSummary());
|
const directives =
|
||||||
|
resolvedDirectives.filter(d => d !== null).map(d => d.metadata.toSummary());
|
||||||
const pipes = ngModule.transitiveModule.pipes.map(
|
const pipes = ngModule.transitiveModule.pipes.map(
|
||||||
p => this.host.resolver.getOrLoadPipeMetadata(p.reference).toSummary());
|
p => this.host.resolver.getOrLoadPipeMetadata(p.reference).toSummary());
|
||||||
const schemas = ngModule.schemas;
|
const schemas = ngModule.schemas;
|
||||||
|
|
|
@ -9,67 +9,125 @@
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {createLanguageService} from './language_service';
|
import {createLanguageService} from './language_service';
|
||||||
import {LanguageService, LanguageServiceHost} from './types';
|
import {Completion, Diagnostic, LanguageService, LanguageServiceHost} from './types';
|
||||||
import {TypeScriptServiceHost} from './typescript_host';
|
import {TypeScriptServiceHost} from './typescript_host';
|
||||||
|
|
||||||
|
export function create(info: any /* ts.server.PluginCreateInfo */): ts.LanguageService {
|
||||||
/** A plugin to TypeScript's langauge service that provide language services for
|
// Create the proxy
|
||||||
* templates in string literals.
|
const proxy: ts.LanguageService = Object.create(null);
|
||||||
*
|
const oldLS: ts.LanguageService = info.languageService;
|
||||||
* @experimental
|
for (const k in oldLS) {
|
||||||
*/
|
(<any>proxy)[k] = function() { return (oldLS as any)[k].apply(oldLS, arguments); };
|
||||||
export class LanguageServicePlugin {
|
|
||||||
private serviceHost: TypeScriptServiceHost;
|
|
||||||
private service: LanguageService;
|
|
||||||
private host: ts.LanguageServiceHost;
|
|
||||||
|
|
||||||
static 'extension-kind' = 'language-service';
|
|
||||||
|
|
||||||
constructor(config: {
|
|
||||||
host: ts.LanguageServiceHost; service: ts.LanguageService;
|
|
||||||
registry?: ts.DocumentRegistry, args?: any
|
|
||||||
}) {
|
|
||||||
this.host = config.host;
|
|
||||||
this.serviceHost = new TypeScriptServiceHost(config.host, config.service);
|
|
||||||
this.service = createLanguageService(this.serviceHost);
|
|
||||||
this.serviceHost.setSite(this.service);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function completionToEntry(c: Completion): ts.CompletionEntry {
|
||||||
* Augment the diagnostics reported by TypeScript with errors from the templates in string
|
return {kind: c.kind, name: c.name, sortText: c.sort, kindModifiers: ''};
|
||||||
* literals.
|
}
|
||||||
*/
|
|
||||||
getSemanticDiagnosticsFilter(fileName: string, previous: ts.Diagnostic[]): ts.Diagnostic[] {
|
function diagnosticToDiagnostic(d: Diagnostic, file: ts.SourceFile): ts.Diagnostic {
|
||||||
let errors = this.service.getDiagnostics(fileName);
|
return {
|
||||||
if (errors && errors.length) {
|
file,
|
||||||
let file = this.serviceHost.getSourceFile(fileName);
|
start: d.span.start,
|
||||||
for (const error of errors) {
|
length: d.span.end - d.span.start,
|
||||||
previous.push({
|
messageText: d.message,
|
||||||
file,
|
category: ts.DiagnosticCategory.Error,
|
||||||
start: error.span.start,
|
code: 0
|
||||||
length: error.span.end - error.span.start,
|
};
|
||||||
messageText: error.message,
|
}
|
||||||
category: ts.DiagnosticCategory.Error,
|
|
||||||
code: 0
|
function tryOperation(attempting: string, callback: () => void) {
|
||||||
});
|
try {
|
||||||
|
callback();
|
||||||
|
} catch (e) {
|
||||||
|
info.project.projectService.logger.info(`Failed to ${attempting}: ${e.toString()}`);
|
||||||
|
info.project.projectService.logger.info(`Stack trace: ${e.stack}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceHost = new TypeScriptServiceHost(info.languageServiceHost, info.languageService);
|
||||||
|
const ls = createLanguageService(serviceHost);
|
||||||
|
serviceHost.setSite(ls);
|
||||||
|
|
||||||
|
proxy.getCompletionsAtPosition = function(fileName: string, position: number) {
|
||||||
|
let base = oldLS.getCompletionsAtPosition(fileName, position);
|
||||||
|
tryOperation('get completions', () => {
|
||||||
|
const results = ls.getCompletionsAt(fileName, position);
|
||||||
|
if (results && results.length) {
|
||||||
|
if (base === undefined) {
|
||||||
|
base = {isMemberCompletion: false, isNewIdentifierLocation: false, entries: []};
|
||||||
|
}
|
||||||
|
for (const entry of results) {
|
||||||
|
base.entries.push(completionToEntry(entry));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
return previous;
|
return base;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
proxy.getQuickInfoAtPosition = function(fileName: string, position: number): ts.QuickInfo {
|
||||||
* Get completions for angular templates if one is at the given position.
|
let base = oldLS.getQuickInfoAtPosition(fileName, position);
|
||||||
*/
|
tryOperation('get quick info', () => {
|
||||||
getCompletionsAtPosition(fileName: string, position: number): ts.CompletionInfo {
|
const ours = ls.getHoverAt(fileName, position);
|
||||||
let result = this.service.getCompletionsAt(fileName, position);
|
if (ours) {
|
||||||
if (result) {
|
const displayParts: typeof base.displayParts = [];
|
||||||
return {
|
for (const part of ours.text) {
|
||||||
isMemberCompletion: false,
|
displayParts.push({kind: part.language, text: part.text});
|
||||||
isNewIdentifierLocation: false,
|
}
|
||||||
entries: result.map<ts.CompletionEntry>(
|
base = {
|
||||||
entry =>
|
displayParts,
|
||||||
({name: entry.name, kind: entry.kind, kindModifiers: '', sortText: entry.sort}))
|
documentation: [],
|
||||||
};
|
kind: 'angular',
|
||||||
|
kindModifiers: 'what does this do?',
|
||||||
|
textSpan: {start: ours.span.start, length: ours.span.end - ours.span.start}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return base;
|
||||||
|
};
|
||||||
|
|
||||||
|
proxy.getSemanticDiagnostics = function(fileName: string) {
|
||||||
|
let base = oldLS.getSemanticDiagnostics(fileName);
|
||||||
|
if (base === undefined) {
|
||||||
|
base = [];
|
||||||
}
|
}
|
||||||
}
|
tryOperation('get diagnostics', () => {
|
||||||
|
info.project.projectService.logger.info(`Computing Angular semantic diagnostics...`);
|
||||||
|
const ours = ls.getDiagnostics(fileName);
|
||||||
|
if (ours && ours.length) {
|
||||||
|
const file = oldLS.getProgram().getSourceFile(fileName);
|
||||||
|
base.push.apply(base, ours.map(d => diagnosticToDiagnostic(d, file)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return base;
|
||||||
|
};
|
||||||
|
|
||||||
|
proxy.getDefinitionAtPosition = function(
|
||||||
|
fileName: string, position: number): ts.DefinitionInfo[] {
|
||||||
|
let base = oldLS.getDefinitionAtPosition(fileName, position);
|
||||||
|
if (base && base.length) {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
tryOperation('get definition', () => {
|
||||||
|
const ours = ls.getDefinitionAt(fileName, position);
|
||||||
|
if (ours && ours.length) {
|
||||||
|
base = base || [];
|
||||||
|
for (const loc of ours) {
|
||||||
|
base.push({
|
||||||
|
fileName: loc.fileName,
|
||||||
|
textSpan: {start: loc.span.start, length: loc.span.end - loc.span.start},
|
||||||
|
name: '',
|
||||||
|
kind: 'definition',
|
||||||
|
containerName: loc.fileName,
|
||||||
|
containerKind: 'file'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return base;
|
||||||
|
};
|
||||||
|
|
||||||
|
return proxy;
|
||||||
}
|
}
|
|
@ -71,12 +71,13 @@ export class MockTypescriptHost implements ts.LanguageServiceHost {
|
||||||
private projectVersion = 0;
|
private projectVersion = 0;
|
||||||
|
|
||||||
constructor(private scriptNames: string[], private data: MockData) {
|
constructor(private scriptNames: string[], private data: MockData) {
|
||||||
let angularIndex = module.filename.indexOf('@angular');
|
const moduleFilename = module.filename.replace(/\\/g, '/');
|
||||||
|
let angularIndex = moduleFilename.indexOf('@angular');
|
||||||
if (angularIndex >= 0)
|
if (angularIndex >= 0)
|
||||||
this.angularPath = module.filename.substr(0, angularIndex).replace('/all/', '/all/@angular/');
|
this.angularPath = moduleFilename.substr(0, angularIndex).replace('/all/', '/all/@angular/');
|
||||||
let distIndex = module.filename.indexOf('/dist/all');
|
let distIndex = moduleFilename.indexOf('/dist/all');
|
||||||
if (distIndex >= 0)
|
if (distIndex >= 0)
|
||||||
this.nodeModulesPath = path.join(module.filename.substr(0, distIndex), 'node_modules');
|
this.nodeModulesPath = path.join(moduleFilename.substr(0, distIndex), 'node_modules');
|
||||||
}
|
}
|
||||||
|
|
||||||
override(fileName: string, content: string) {
|
override(fileName: string, content: string) {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import 'reflect-metadata';
|
||||||
|
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {LanguageServicePlugin} from '../src/ts_plugin';
|
import {create} from '../src/ts_plugin';
|
||||||
|
|
||||||
import {toh} from './test_data';
|
import {toh} from './test_data';
|
||||||
import {MockTypescriptHost} from './test_utils';
|
import {MockTypescriptHost} from './test_utils';
|
||||||
|
@ -21,6 +21,8 @@ describe('plugin', () => {
|
||||||
let service = ts.createLanguageService(mockHost, documentRegistry);
|
let service = ts.createLanguageService(mockHost, documentRegistry);
|
||||||
let program = service.getProgram();
|
let program = service.getProgram();
|
||||||
|
|
||||||
|
const mockProject = {projectService: {logger: {info: function() {}}}};
|
||||||
|
|
||||||
it('should not report errors on tour of heroes', () => {
|
it('should not report errors on tour of heroes', () => {
|
||||||
expectNoDiagnostics(service.getCompilerOptionsDiagnostics());
|
expectNoDiagnostics(service.getCompilerOptionsDiagnostics());
|
||||||
for (let source of program.getSourceFiles()) {
|
for (let source of program.getSourceFiles()) {
|
||||||
|
@ -29,13 +31,15 @@ describe('plugin', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let plugin = new LanguageServicePlugin({host: mockHost, service, registry: documentRegistry});
|
|
||||||
|
let plugin = create(
|
||||||
|
{ts: ts, languageService: service, project: mockProject, languageServiceHost: mockHost});
|
||||||
|
|
||||||
it('should not report template errors on tour of heroes', () => {
|
it('should not report template errors on tour of heroes', () => {
|
||||||
for (let source of program.getSourceFiles()) {
|
for (let source of program.getSourceFiles()) {
|
||||||
// Ignore all 'cases.ts' files as they intentionally contain errors.
|
// Ignore all 'cases.ts' files as they intentionally contain errors.
|
||||||
if (!source.fileName.endsWith('cases.ts')) {
|
if (!source.fileName.endsWith('cases.ts')) {
|
||||||
expectNoDiagnostics(plugin.getSemanticDiagnosticsFilter(source.fileName, []));
|
expectNoDiagnostics(plugin.getSemanticDiagnostics(source.fileName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -109,8 +113,6 @@ describe('plugin', () => {
|
||||||
describe('with a *ngFor', () => {
|
describe('with a *ngFor', () => {
|
||||||
it('should include a let for empty attribute',
|
it('should include a let for empty attribute',
|
||||||
() => { contains('app/parsing-cases.ts', 'for-empty', 'let'); });
|
() => { contains('app/parsing-cases.ts', 'for-empty', 'let'); });
|
||||||
it('should not suggest any entries if in the name part of a let',
|
|
||||||
() => { expectEmpty('app/parsing-cases.ts', 'for-let-empty'); });
|
|
||||||
it('should suggest NgForRow members for let initialization expression', () => {
|
it('should suggest NgForRow members for let initialization expression', () => {
|
||||||
contains(
|
contains(
|
||||||
'app/parsing-cases.ts', 'for-let-i-equal', 'index', 'count', 'first', 'last', 'even',
|
'app/parsing-cases.ts', 'for-let-i-equal', 'index', 'count', 'first', 'last', 'even',
|
||||||
|
@ -206,13 +208,13 @@ describe('plugin', () => {
|
||||||
|
|
||||||
function expectEmpty(fileName: string, locationMarker: string) {
|
function expectEmpty(fileName: string, locationMarker: string) {
|
||||||
const location = getMarkerLocation(fileName, locationMarker);
|
const location = getMarkerLocation(fileName, locationMarker);
|
||||||
expect(plugin.getCompletionsAtPosition(fileName, location).entries).toEqual([]);
|
expect(plugin.getCompletionsAtPosition(fileName, location).entries || []).toEqual([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function expectSemanticError(fileName: string, locationMarker: string, message: string) {
|
function expectSemanticError(fileName: string, locationMarker: string, message: string) {
|
||||||
const start = getMarkerLocation(fileName, locationMarker);
|
const start = getMarkerLocation(fileName, locationMarker);
|
||||||
const end = getMarkerLocation(fileName, locationMarker + '-end');
|
const end = getMarkerLocation(fileName, locationMarker + '-end');
|
||||||
const errors = plugin.getSemanticDiagnosticsFilter(fileName, []);
|
const errors = plugin.getSemanticDiagnostics(fileName);
|
||||||
for (const error of errors) {
|
for (const error of errors) {
|
||||||
if (error.messageText.toString().indexOf(message) >= 0) {
|
if (error.messageText.toString().indexOf(message) >= 0) {
|
||||||
expect(error.start).toEqual(start);
|
expect(error.start).toEqual(start);
|
||||||
|
@ -220,8 +222,9 @@ describe('plugin', () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(`Expected error messages to contain ${message}, in messages:\n ${errors
|
||||||
`Expected error messages to contain ${message}, in messages:\n ${errors.map(e => e.messageText.toString()).join(',\n ')}`);
|
.map(e => e.messageText.toString())
|
||||||
|
.join(',\n ')}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -229,8 +232,8 @@ describe('plugin', () => {
|
||||||
function expectEntries(locationMarker: string, info: ts.CompletionInfo, ...names: string[]) {
|
function expectEntries(locationMarker: string, info: ts.CompletionInfo, ...names: string[]) {
|
||||||
let entries: {[name: string]: boolean} = {};
|
let entries: {[name: string]: boolean} = {};
|
||||||
if (!info) {
|
if (!info) {
|
||||||
throw new Error(
|
throw new Error(`Expected result from ${locationMarker} to include ${names.join(
|
||||||
`Expected result from ${locationMarker} to include ${names.join(', ')} but no result provided`);
|
', ')} but no result provided`);
|
||||||
} else {
|
} else {
|
||||||
for (let entry of info.entries) {
|
for (let entry of info.entries) {
|
||||||
entries[entry.name] = true;
|
entries[entry.name] = true;
|
||||||
|
@ -240,12 +243,15 @@ function expectEntries(locationMarker: string, info: ts.CompletionInfo, ...names
|
||||||
let missing = shouldContains.filter(name => !entries[name]);
|
let missing = shouldContains.filter(name => !entries[name]);
|
||||||
let present = shouldNotContain.map(name => name.substr(1)).filter(name => entries[name]);
|
let present = shouldNotContain.map(name => name.substr(1)).filter(name => entries[name]);
|
||||||
if (missing.length) {
|
if (missing.length) {
|
||||||
throw new Error(
|
throw new Error(`Expected result from ${locationMarker
|
||||||
`Expected result from ${locationMarker} to include at least one of the following, ${missing.join(', ')}, in the list of entries ${info.entries.map(entry => entry.name).join(', ')}`);
|
} to include at least one of the following, ${missing
|
||||||
|
.join(', ')}, in the list of entries ${info.entries.map(entry => entry.name)
|
||||||
|
.join(', ')}`);
|
||||||
}
|
}
|
||||||
if (present.length) {
|
if (present.length) {
|
||||||
throw new Error(
|
throw new Error(`Unexpected member${present.length > 1 ? 's' :
|
||||||
`Unexpected member${present.length > 1 ? 's': ''} included in result: ${present.join(', ')}`);
|
''
|
||||||
|
} included in result: ${present.join(', ')}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue