diff --git a/src/compiler/extensions.ts b/src/compiler/extensions.ts index db0f3aaa4f3e6..a4ea4905d32e6 100644 --- a/src/compiler/extensions.ts +++ b/src/compiler/extensions.ts @@ -32,17 +32,30 @@ namespace ts { new (state: {ts: typeof ts, args: any, host: CompilerHost, program: Program, checker: TypeChecker}): LintWalker; } + export interface LanguageServiceHost {} // The members for these interfaces are provided in the services layer + export interface LanguageService {} + export interface LanguageServiceProvider {} + export interface DocumentRegistry {} + + export interface LanguageServiceProviderStatic extends BaseProviderStatic { + readonly ["extension-kind"]: ExtensionKind.LanguageService; + new (state: { ts: typeof ts, args: any, host: LanguageServiceHost, service: LanguageService, registry: DocumentRegistry }): LanguageServiceProvider; + } + export namespace ExtensionKind { export const SemanticLint: "semantic-lint" = "semantic-lint"; export type SemanticLint = "semantic-lint"; export const SyntacticLint: "syntactic-lint" = "syntactic-lint"; export type SyntacticLint = "syntactic-lint"; + export const LanguageService: "language-service" = "language-service"; + export type LanguageService = "language-service"; } - export type ExtensionKind = ExtensionKind.SemanticLint | ExtensionKind.SyntacticLint; + export type ExtensionKind = ExtensionKind.SemanticLint | ExtensionKind.SyntacticLint | ExtensionKind.LanguageService; export interface ExtensionCollectionMap { "syntactic-lint"?: SyntacticLintExtension[]; "semantic-lint"?: SemanticLintExtension[]; + "language-service"?: LanguageServiceExtension[]; [index: string]: Extension[] | undefined; } @@ -62,7 +75,12 @@ namespace ts { ctor: SemanticLintProviderStatic; } - export type Extension = SyntacticLintExtension | SemanticLintExtension; + // @kind(ExtensionKind.LanguageService) + export interface LanguageServiceExtension extends ExtensionBase { + ctor: LanguageServiceProviderStatic; + } + + export type Extension = SyntacticLintExtension | SemanticLintExtension | LanguageServiceExtension; export interface ExtensionCache { getCompilerExtensions(): ExtensionCollectionMap; @@ -141,6 +159,7 @@ namespace ts { switch (ext.kind) { case ExtensionKind.SemanticLint: case ExtensionKind.SyntacticLint: + case ExtensionKind.LanguageService: if (typeof potentialExtension !== "function") { diagnostics.push(createCompilerDiagnostic( Diagnostics.Extension_0_exported_member_1_has_extension_kind_2_but_was_type_3_when_type_4_was_expected, @@ -152,7 +171,7 @@ namespace ts { )); return; } - (ext as (SemanticLintExtension | SyntacticLintExtension)).ctor = potentialExtension as (SemanticLintProviderStatic | SyntacticLintProviderStatic); + (ext as (SemanticLintExtension | SyntacticLintExtension | LanguageServiceExtension)).ctor = potentialExtension as (SemanticLintProviderStatic | SyntacticLintProviderStatic | LanguageServiceProviderStatic); break; default: // Include a default case which just puts the extension unchecked onto the base extension diff --git a/src/services/services.ts b/src/services/services.ts index 2099db112f67c..098a97e0748e6 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1163,6 +1163,86 @@ namespace ts { dispose(): void; } + export interface LanguageServiceProvider { + // Overrides + + // A plugin can implement one of the override methods to replace the results that would + // be returned by the TypeScript language service. If a plugin returns a defined results + // (that is, is not undefined) then that result is used instead of invoking the + // corresponding TypeScript method. If multiple plugins are registered, they are + // consulted in the order they are returned from the program. The first defined result + // returned by a plugin is used and no other plugin overrides are consulted. + + getProgramDiagnostics?(): Diagnostic[]; + getSyntacticDiagnostics?(fileName: string): Diagnostic[]; + getSemanticDiagnostics?(fileName: string): Diagnostic[]; + getEncodedSyntacticClassifications?(fileName: string, span: TextSpan): Classifications; + getEncodedSemanticClassifications?(fileName: string, span: TextSpan): Classifications; + getCompletionsAtPosition?(fileName: string, position: number): CompletionInfo; + getCompletionEntryDetails?(fileName: string, position: number, entryName: string): CompletionEntryDetails; + getQuickInfoAtPosition?(fileName: string, position: number): QuickInfo; + getNameOrDottedNameSpan?(fileName: string, startPos: number, endPos: number): TextSpan; + getBreakpointStatementAtPosition?(fileName: string, position: number): TextSpan; + getSignatureHelpItems?(fileName: string, position: number): SignatureHelpItems; + getRenameInfo?(fileName: string, position: number): RenameInfo; + findRenameLocations?(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): RenameLocation[]; + getDefinitionAtPosition?(fileName: string, position: number): DefinitionInfo[]; + getTypeDefinitionAtPosition?(fileName: string, position: number): DefinitionInfo[]; + getReferencesAtPosition?(fileName: string, position: number): ReferenceEntry[]; + findReferences?(fileName: string, position: number): ReferencedSymbol[]; + getDocumentHighlights?(fileName: string, position: number, filesToSearch: string[]): DocumentHighlights[]; + getNavigateToItems?(searchValue: string, maxResultCount: number): NavigateToItem[]; + getNavigationBarItems?(fileName: string): NavigationBarItem[]; + getOutliningSpans?(fileName: string): OutliningSpan[]; + getTodoComments?(fileName: string, descriptors: TodoCommentDescriptor[]): TodoComment[]; + getBraceMatchingAtPosition?(fileName: string, position: number): TextSpan[]; + getIndentationAtPosition?(fileName: string, position: number, options: EditorOptions): number; + getFormattingEditsForRange?(fileName: string, start: number, end: number, options: FormatCodeOptions): TextChange[]; + getFormattingEditsForDocument?(fileName: string, options: FormatCodeOptions): TextChange[]; + getFormattingEditsAfterKeystroke?(fileName: string, position: number, key: string, options: FormatCodeOptions): TextChange[]; + getDocCommentTemplateAtPosition?(fileName: string, position: number): TextInsertion; + + // Filters + + // A plugin can implement one of the filter methods to augment, extend or modify a result + // prior to the host receiving it. The TypeScript language service is invoked and the + // result is passed to the plugin as the value of the previous parameter. If more than one + // plugin is registered, the plugins are consulted in the order they are returned from the + // program. The value passed in as previous is the result returned by the prior plugin. If a + // plugin returns undefined, the result passed in as previous is used and the undefined + // result is ignored. All plugins are consulted before the result is returned to the host. + // If a plugin overrides behavior of the method, no filter methods are consulted. + + getProgramDiagnosticsFilter?(previous: Diagnostic[]): Diagnostic[]; + getSyntacticDiagnosticsFilter?(fileName: string, previous: Diagnostic[]): Diagnostic[]; + getSemanticDiagnosticsFilter?(fileName: string, previous: Diagnostic[]): Diagnostic[]; + getEncodedSyntacticClassificationsFilter?(fileName: string, span: TextSpan, previous: Classifications): Classifications; + getEncodedSemanticClassificationsFilter?(fileName: string, span: TextSpan, previous: Classifications): Classifications; + getCompletionsAtPositionFilter?(fileName: string, position: number, previous: CompletionInfo): CompletionInfo; + getCompletionEntryDetailsFilter?(fileName: string, position: number, entryName: string, previous: CompletionEntryDetails): CompletionEntryDetails; + getQuickInfoAtPositionFilter?(fileName: string, position: number, previous: QuickInfo): QuickInfo; + getNameOrDottedNameSpanFilter?(fileName: string, startPos: number, endPos: number, previous: TextSpan): TextSpan; + getBreakpointStatementAtPositionFilter?(fileName: string, position: number, previous: TextSpan): TextSpan; + getSignatureHelpItemsFilter?(fileName: string, position: number, previous: SignatureHelpItems): SignatureHelpItems; + getRenameInfoFilter?(fileName: string, position: number, previous: RenameInfo): RenameInfo; + findRenameLocationsFilter?(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, previous: RenameLocation[]): RenameLocation[]; + getDefinitionAtPositionFilter?(fileName: string, position: number, previous: DefinitionInfo[]): DefinitionInfo[]; + getTypeDefinitionAtPositionFilter?(fileName: string, position: number, previous: DefinitionInfo[]): DefinitionInfo[]; + getReferencesAtPositionFilter?(fileName: string, position: number, previous: ReferenceEntry[]): ReferenceEntry[]; + findReferencesFilter?(fileName: string, position: number, previous: ReferencedSymbol[]): ReferencedSymbol[]; + getDocumentHighlightsFilter?(fileName: string, position: number, filesToSearch: string[], previous: DocumentHighlights[]): DocumentHighlights[]; + getNavigateToItemsFilter?(searchValue: string, maxResultCount: number, previous: NavigateToItem[]): NavigateToItem[]; + getNavigationBarItemsFilter?(fileName: string, previous: NavigationBarItem[]): NavigationBarItem[]; + getOutliningSpansFilter?(fileName: string, previous: OutliningSpan[]): OutliningSpan[]; + getTodoCommentsFilter?(fileName: string, descriptors: TodoCommentDescriptor[], previous: TodoComment[]): TodoComment[]; + getBraceMatchingAtPositionFilter?(fileName: string, position: number, previous: TextSpan[]): TextSpan[]; + getIndentationAtPositionFilter?(fileName: string, position: number, options: EditorOptions, previous: number): number; + getFormattingEditsForRangeFilter?(fileName: string, start: number, end: number, options: FormatCodeOptions, previous: TextChange[]): TextChange[]; + getFormattingEditsForDocumentFilter?(fileName: string, options: FormatCodeOptions, previous: TextChange[]): TextChange[]; + getFormattingEditsAfterKeystrokeFilter?(fileName: string, position: number, key: string, options: FormatCodeOptions, previous: TextChange[]): TextChange[]; + getDocCommentTemplateAtPositionFilter?(fileName: string, position: number, previous: TextInsertion): TextInsertion; + } + export interface Classifications { spans: number[]; endOfLineState: EndOfLineState; @@ -2920,6 +3000,76 @@ namespace ts { } export function createLanguageService(host: LanguageServiceHost, + documentRegistry: DocumentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames(), host.getCurrentDirectory())): LanguageService { + const baseService = createUnextendedLanguageService(host, documentRegistry); + const extensions = baseService.getProgram().getCompilerExtensions()["language-service"]; + const instantiatedExtensions = map(extensions, extension => new extension.ctor({ ts, host, service: baseService, registry: documentRegistry, args: extension.args })); + const extensionCount = instantiatedExtensions && instantiatedExtensions.length; + + function wrap(key: string): Function { + if (extensionCount) { + return (...args: any[]) => { + for (let i = 0; i < extensionCount; i++) { + const extension = instantiatedExtensions[i]; + if ((extension as any)[key]) { + const temp = (extension as any)[key](...args); + if (temp !== undefined) { + return temp; + } + } + } + let result: any = (baseService as any)[key](...args); + const filterKey = `${key}Filter`; + for (let i = 0; i < extensionCount; i++) { + const extension = instantiatedExtensions[i]; + if ((extension as any)[filterKey]) { + const temp = (extension as any)[filterKey](...args, result); + if (temp !== undefined) { + result = temp; + } + } + } + return result; + }; + } + return (baseService as any)[key]; + } + + function buildWrappedService(underlyingMembers: Map, wrappedMembers: string[]): LanguageService { + // Add wrapped members to map + forEach(wrappedMembers, member => { + underlyingMembers[member] = wrap(member); + }); + // Map getProgramDiagnostics to deprecated getCompilerOptionsDiagnostics + underlyingMembers["getCompilerOptionsDiagnostics"] = underlyingMembers["getProgramDiagnostics"]; + return underlyingMembers as LanguageService; + } + + return buildWrappedService({ + cleanupSemanticCache: () => baseService.cleanupSemanticCache(), + getSyntacticClassifications: (fileName: string, span: TextSpan) => baseService.getSyntacticClassifications(fileName, span), + getSemanticClassifications: (fileName: string, span: TextSpan) => baseService.getSemanticClassifications(fileName, span), + getOccurrencesAtPosition: (fileName: string, position: number) => baseService.getOccurrencesAtPosition(fileName, position), + isValidBraceCompletionAtPosition: (fileName: string, pos: number, openingBrace: number) => baseService.isValidBraceCompletionAtPosition(fileName, pos, openingBrace), + getEmitOutput: (fileName: string) => baseService.getEmitOutput(fileName), + getProgram: () => baseService.getProgram(), + getNonBoundSourceFile: (fileName: string) => baseService.getNonBoundSourceFile(fileName), + dispose: () => baseService.dispose(), + }, [ + "getSyntacticDiagnostics", "getSemanticDiagnostics", "getProgramDiagnostics", + "getEncodedSyntacticClassifications", "getEncodedSemanticClassifications", "getCompletionsAtPosition", + "getCompletionEntryDetails", "getQuickInfoAtPosition", "getNameOrDottedNameSpan", + "getBreakpointStatementAtPosition", "getSignatureHelpItems", "getRenameInfo", + "findRenameLocations", "getDefinitionAtPosition", "getTypeDefinitionAtPosition", + "getReferencesAtPosition", "findReferences", "getDocumentHighlights", + "getNavigateToItems", "getNavigationBarItems", "getOutliningSpans", + "getTodoComments", "getBraceMatchingAtPosition", "getIndentationAtPosition", + "getFormattingEditsForRange", "getFormattingEditsForDocument", "getFormattingEditsAfterKeystroke", + "getDocCommentTemplateAtPosition" + ]); + } + + export function createUnextendedLanguageService(host: LanguageServiceHost, documentRegistry: DocumentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames(), host.getCurrentDirectory())): LanguageService { const syntaxTreeCache: SyntaxTreeCache = new SyntaxTreeCache(host); diff --git a/tests/cases/unittests/extensionAPI.ts b/tests/cases/unittests/extensionAPI.ts index 505a92e39dea7..aea114919b363 100644 --- a/tests/cases/unittests/extensionAPI.ts +++ b/tests/cases/unittests/extensionAPI.ts @@ -194,6 +194,22 @@ export abstract class SemanticLintWalker implements tsi.LintWalker { } abstract visit(node: tsi.Node, stop: tsi.LintStopMethod, error: tsi.LintErrorMethod): void; } + +export abstract class LanguageServiceProvider implements tsi.LanguageServiceProvider { + static "extension-kind": tsi.ExtensionKind.LanguageService = "language-service"; + protected ts: typeof tsi; + protected args: any; + protected host: tsi.LanguageServiceHost; + protected service: tsi.LanguageService; + protected registry: tsi.DocumentRegistry; + constructor(state: {ts: typeof tsi, args: any, host: tsi.LanguageServiceHost, service: tsi.LanguageService, registry: tsi.DocumentRegistry}) { + this.ts = state.ts; + this.args = state.args; + this.host = state.host; + this.service = state.service; + this.registry = state.registry; + } +} ` }; // Compile extension API once (generating .d.ts and .js) @@ -375,6 +391,417 @@ export class IsValueBar extends SemanticLintWalker { } } } +` + }, + "test-language-service": { + "package.json": `{ + "name": "test-language-service", + "version": "1.0.0", + "description": "", + "main": "index.js", + "author": "" +}`, + "index.ts": ` +import {LanguageServiceProvider} from "typescript-plugin-api"; + +export default class extends LanguageServiceProvider { + constructor(state) { super(state); } + getProgramDiagnosticsFilter(previous) { + previous.push({ + file: undefined, + start: undefined, + length: undefined, + messageText: "Test language service plugin loaded!", + category: 2, + code: "test-plugin-loaded", + }); + return previous; + } +} +` + }, + "test-service-overrides": { + "package.json": `{ + "name": "test-service-overrides", + "version": "1.0.0", + "description": "", + "main": "index.js", + "author": "" +}`, + "index.ts": ` +import {LanguageServiceProvider} from "typescript-plugin-api"; +import * as ts from "typescript"; + +export default class extends LanguageServiceProvider { + constructor(state) { super(state); } + getProgramDiagnostics() { + return [{ + file: undefined, + start: undefined, + length: undefined, + messageText: "Program diagnostics replaced!", + category: 2, + code: "program-diagnostics-replaced", + }]; + } + getSyntacticDiagnostics(fileName) { + return [{ + file: undefined, + start: undefined, + length: undefined, + messageText: "Syntactic diagnostics replaced!", + category: 2, + code: "syntactic-diagnostics-replaced", + }]; + } + getSemanticDiagnostics(fileName) { + return [{ + file: undefined, + start: undefined, + length: undefined, + messageText: "Semantic diagnostics replaced!", + category: 2, + code: "semantic-diagnostics-replaced", + }]; + } + getEncodedSyntacticClassifications(fileName, span) { + return { + spans: [span.start, span.length, this.ts.endsWith(fileName, "atotc.ts") ? this.ts.ClassificationType.text : this.ts.ClassificationType.comment], + endOfLineState: this.ts.EndOfLineState.None + }; + } + getEncodedSemanticClassifications(fileName, span) { + return { + spans: [span.start, span.length, this.ts.endsWith(fileName, "atotc.ts") ? this.ts.ClassificationType.moduleName : this.ts.ClassificationType.comment], + endOfLineState: this.ts.EndOfLineState.None + }; + } + getCompletionsAtPosition(fileName, position) { + return { + isMemberCompletion: false, + isNewIdentifierLocation: false, + entries: [{name: fileName, kind: "", kindModifiers: "", sortText: fileName}] + }; + } + getCompletionEntryDetails(fileName, position, entryName) { + return { + name: fileName, + kind: position.toString(), + kindModifiers: entryName, + displayParts: [], + documentation: [], + }; + } + getQuickInfoAtPosition(fileName, position) { + return {}; + } + getNameOrDottedNameSpan(fileName, startPos, endPos) { + return {}; + } + getBreakpointStatementAtPosition(fileName, position) { + return {}; + } + getSignatureHelpItems(fileName, position) { + return {}; + } + getRenameInfo(fileName, position) { + return {}; + } + findRenameLocations(fileName, position, findInStrings, findInComments) { + return {}; + } + getDefinitionAtPosition(fileName, position) { + return {}; + } + getTypeDefinitionAtPosition(fileName, position) { + return {}; + } + getReferencesAtPosition(fileName, position) { + return {}; + } + findReferences(fileName, position) { + return {}; + } + getDocumentHighlights(fileName, position, filesToSearch) { + return {}; + } + getNavigateToItems(searchValue, maxResultCount) { + return {}; + } + getNavigationBarItems(fileName) { + return {}; + } + getOutliningSpans(fileName) { + return {}; + } + getTodoComments(fileName, descriptors) { + return {}; + } + getBraceMatchingAtPosition(fileName, position) { + return {}; + } + getIndentationAtPosition(fileName, position, options) { + return {}; + } + getFormattingEditsForRange(fileName, start, end, options) { + return {}; + } + getFormattingEditsForDocument(fileName, options) { + return {}; + } + getFormattingEditsAfterKeystroke(fileName, position, key, options) { + return {}; + } + getDocCommentTemplateAtPosition(fileName, position) { + return {}; + } +} +` + }, + "test-service-filters": { + "package.json": `{ + "name": "test-service-filters", + "version": "1.0.0", + "description": "", + "main": "index.js", + "author": "" +}`, + "index.ts": ` +import {LanguageServiceProvider} from "typescript-plugin-api"; + +import * as ts from "typescript"; + +export default class extends LanguageServiceProvider { + constructor(state) { super(state); } + getProgramDiagnosticsFilter(previous) { + return [{ + file: undefined, + start: undefined, + length: undefined, + messageText: "Program diagnostics replaced!", + category: 2, + code: "program-diagnostics-replaced", + }]; + } + getSyntacticDiagnosticsFilter(fileName, previous) { + return [{ + file: undefined, + start: undefined, + length: undefined, + messageText: "Syntactic diagnostics replaced!", + category: 2, + code: "syntactic-diagnostics-replaced", + }]; + } + getSemanticDiagnosticsFilter(fileName, previous) { + return [{ + file: undefined, + start: undefined, + length: undefined, + messageText: "Semantic diagnostics replaced!", + category: 2, + code: "semantic-diagnostics-replaced", + }]; + } + getEncodedSyntacticClassificationsFilter(fileName, span, previous) { + return { + spans: [span.start, span.length, this.ts.endsWith(fileName, "atotc.ts") ? this.ts.ClassificationType.text : this.ts.ClassificationType.comment], + endOfLineState: this.ts.EndOfLineState.None + }; + } + getEncodedSemanticClassificationsFilter(fileName, span, previous) { + return { + spans: [span.start, span.length, this.ts.endsWith(fileName, "atotc.ts") ? this.ts.ClassificationType.moduleName : this.ts.ClassificationType.comment], + endOfLineState: this.ts.EndOfLineState.None + }; + } + getCompletionsAtPositionFilter(fileName, position, previous) { + return { + isMemberCompletion: false, + isNewIdentifierLocation: false, + entries: [{name: fileName, kind: "", kindModifiers: "", sortText: fileName}] + }; + } + getCompletionEntryDetailsFilter(fileName, position, entryName, previous) { + return { + name: fileName, + kind: position.toString(), + kindModifiers: entryName, + displayParts: [], + documentation: [], + }; + } + getQuickInfoAtPositionFilter(fileName, position, previous) { + return {}; + } + getNameOrDottedNameSpanFilter(fileName, startPos, endPos, previous) { + return {}; + } + getBreakpointStatementAtPositionFilter(fileName, position, previous) { + return {}; + } + getSignatureHelpItemsFilter(fileName, position, previous) { + return {}; + } + getRenameInfoFilter(fileName, position, previous) { + return {}; + } + findRenameLocationsFilter(fileName, position, findInStrings, findInComments, previous) { + return {}; + } + getDefinitionAtPositionFilter(fileName, position, previous) { + return {}; + } + getTypeDefinitionAtPositionFilter(fileName, position, previous) { + return {}; + } + getReferencesAtPositionFilter(fileName, position, previous) { + return {}; + } + findReferencesFilter(fileName, position, previous) { + return {}; + } + getDocumentHighlightsFilter(fileName, position, filesToSearch, previous) { + return {}; + } + getNavigateToItemsFilter(searchValue, maxResultCount, previous) { + return {}; + } + getNavigationBarItemsFilter(fileName, previous) { + return {}; + } + getOutliningSpansFilter(fileName, previous) { + return {}; + } + getTodoCommentsFilter(fileName, descriptors, previous) { + return {}; + } + getBraceMatchingAtPositionFilter(fileName, position, previous) { + return {}; + } + getIndentationAtPositionFilter(fileName, position, options, previous) { + return {}; + } + getFormattingEditsForRangeFilter(fileName, start, end, options, previous) { + return {}; + } + getFormattingEditsForDocumentFilter(fileName, options, previous) { + return {}; + } + getFormattingEditsAfterKeystrokeFilter(fileName, position, key, options, previous) { + return {}; + } + getDocCommentTemplateAtPositionFilter(fileName, position, previous) { + return {}; + } +} +` + }, + "test-service-passthru": { + "package.json": `{ + "name": "test-service-passthru", + "version": "1.0.0", + "description": "", + "main": "index.js", + "author": "" +}`, + "index.ts": ` +import {LanguageServiceProvider} from "typescript-plugin-api"; + +export default class extends LanguageServiceProvider { + constructor(state) { super(state); } + getProgramDiagnosticsFilter(previous) { + return previous; + } + getSyntacticDiagnosticsFilter(fileName, previous) { + return previous; + } + getSemanticDiagnosticsFilter(fileName, previous) { + return previous; + } +} +` + }, + "test-service-chain": { + "package.json": `{ + "name": "test-service-chain", + "version": "1.0.0", + "description": "", + "main": "index.js", + "author": "" +}`, + "index.ts": ` +import {LanguageServiceProvider} from "typescript-plugin-api"; + +export class AddsDiagnostics extends LanguageServiceProvider { + constructor(state) { super(state); } + getProgramDiagnosticsFilter(previous) { + return previous.concat([{ + file: undefined, + start: undefined, + length: undefined, + messageText: "Program diagnostics amended!", + category: 2, + code: "program-diagnostics-amended", + }]); + } + getSyntacticDiagnosticsFilter(fileName, previous) { + return previous.concat([{ + file: undefined, + start: undefined, + length: undefined, + messageText: "Syntactic diagnostics amended!", + category: 2, + code: "syntactic-diagnostics-amended", + }]); + } + getSemanticDiagnosticsFilter(fileName, previous) { + return previous.concat([{ + file: undefined, + start: undefined, + length: undefined, + messageText: "Semantic diagnostics amended!", + category: 2, + code: "semantic-diagnostics-amended", + }]); + } +} + +// Since this is exported second, it should be second in the chain. Probably. +// This is honestly dependent on js host key ordering +export class MutatesAddedDiagnostics extends LanguageServiceProvider { + constructor(state) { super(state); } + getProgramDiagnosticsFilter(previous) { + return previous.map(prev => prev.code === "program-diagnostics-amended" ? { + file: undefined, + start: undefined, + length: undefined, + messageText: "Program diagnostics mutated!", + category: 2, + code: "program-diagnostics-mutated", + } : prev); + } + getSyntacticDiagnosticsFilter(fileName, previous) { + return previous.map(prev => prev.code === "syntactic-diagnostics-amended" ? { + file: undefined, + start: undefined, + length: undefined, + messageText: "Syntactic diagnostics mutated!", + category: 2, + code: "syntactic-diagnostics-mutated", + } : prev); + } + getSemanticDiagnosticsFilter(fileName, previous) { + return previous.map(prev => prev.code === "semantic-diagnostics-amended" ? { + file: undefined, + start: undefined, + length: undefined, + messageText: "Semantic diagnostics mutated!", + category: 2, + code: "semantic-diagnostics-mutated", + } : prev); + } +} ` }, "test-multierrors": { @@ -662,6 +1089,128 @@ const x = 3 * 4; } }); }); + + it("can load language service rules and add program diagnostics", () => { + test({ + "main.ts": "console.log('Did you know? The empty string is falsey.')" + }, { + availableExtensions: ["test-language-service"], + expectedDiagnostics: ["test-plugin-loaded"], + compilerOptions: { + extensions: ["test-language-service"], + module: ModuleKind.CommonJS, + } + }, languageServiceCompile); + }); + + const atotcFile = getCanonicalFileName("atotc.ts"); + const atotcText = ` +It was the best of times, it was the worst of times, +it was the age of wisdom, it was the age of foolishness, +it was the epoch of belief, it was the epoch of incredulity, +it was the season of Light, it was the season of Darkness, +it was the spring of hope, it was the winter of despair, +we had everything before us, we had nothing before us, +we were all going direct to Heaven, we were all going direct +the other way--in short, the period was so far like the present +period, that some of its noisiest authorities insisted on its +being received, for good or for evil, in the superlative degree +of comparison only. +`; + const testDummyLS = (service: LanguageService) => { + assert.deepEqual(service.getEncodedSyntacticClassifications(atotcFile, { start: 0, length: 24 }), + { spans: [0, 24, ClassificationType.text], endOfLineState: EndOfLineState.None }, + "Syntactic classifications did not match!"); + assert.deepEqual(service.getEncodedSemanticClassifications(atotcFile, { start: 24, length: 42 }), + { spans: [24, 42, ClassificationType.moduleName], endOfLineState: EndOfLineState.None }, + "Semantic classifications did not match!"); + assert.deepEqual(service.getCompletionsAtPosition(atotcFile, 0), { + isMemberCompletion: false, + isNewIdentifierLocation: false, + entries: [{ name: atotcFile as Path, kind: "", kindModifiers: "", sortText: atotcFile }] + }, "Completions did not match!"); + assert.deepEqual(service.getCompletionEntryDetails(atotcFile, 0, "first"), { + name: atotcFile, + kind: (0).toString(), + kindModifiers: "first", + displayParts: [], + documentation: [], + }, "Completion details did not match!"); + assert.deepEqual(service.getQuickInfoAtPosition(atotcFile, 0), {}, "Quick info did not match!"); + assert.deepEqual(service.getNameOrDottedNameSpan(atotcFile, 0, 0), {}, "Name or dotted span info did not match!"); + assert.deepEqual(service.getBreakpointStatementAtPosition(atotcFile, 0), {}, "Breakpoint statement info did not match!"); + assert.deepEqual(service.getSignatureHelpItems(atotcFile, 0), {}, "Signature help items did not match!"); + assert.deepEqual(service.getRenameInfo(atotcFile, 0), {}, "Rename info did not match!"); + assert.deepEqual(service.findRenameLocations(atotcFile, 0, false, false), {}, "Rename locations did not match!"); + assert.deepEqual(service.getDefinitionAtPosition(atotcFile, 0), {}, "Definition info did not match!"); + assert.deepEqual(service.getTypeDefinitionAtPosition(atotcFile, 0), {}, "Type definition info did not match!"); + assert.deepEqual(service.getReferencesAtPosition(atotcFile, 0), {}, "References did not match!"); + assert.deepEqual(service.findReferences(atotcFile, 0), {}, "Find references did not match!"); + assert.deepEqual(service.getDocumentHighlights(atotcFile, 0, []), {}, "Document highlights did not match!"); + assert.deepEqual(service.getNavigateToItems(atotcFile), {}, "NavTo items did not match!"); + assert.deepEqual(service.getNavigationBarItems(atotcFile), {}, "NavBar items did not match!"); + assert.deepEqual(service.getOutliningSpans(atotcFile), {}, "Outlining spans did not match!"); + assert.deepEqual(service.getTodoComments(atotcFile, []), {}, "Todo comments did not match!"); + assert.deepEqual(service.getBraceMatchingAtPosition(atotcFile, 0), {}, "Brace positions did not match!"); + assert.deepEqual(service.getIndentationAtPosition(atotcFile, 0, {} as EditorOptions), {}, "Indentation positions did not match!"); + assert.deepEqual(service.getFormattingEditsForRange(atotcFile, 0, 1, {} as FormatCodeOptions), {}, "Range edits did not match!"); + assert.deepEqual(service.getFormattingEditsForDocument(atotcFile, {} as FormatCodeOptions), {}, "Document edits did not match!"); + assert.deepEqual(service.getFormattingEditsAfterKeystroke(atotcFile, 0, "q", {} as FormatCodeOptions), {}, "Keystroke edits did not match!"); + assert.deepEqual(service.getDocCommentTemplateAtPosition(atotcFile, 0), {}, "Doc comment template did not match!"); + }; + + it("can override all language service functionality", () => { + test({ + [atotcFile]: atotcText + }, { + availableExtensions: ["test-service-overrides"], + expectedDiagnostics: ["program-diagnostics-replaced", "semantic-diagnostics-replaced", "syntactic-diagnostics-replaced"], + compilerOptions: { + extensions: ["test-service-overrides"], + module: ModuleKind.CommonJS, + } + }, languageServiceCompile, testDummyLS); + }); + + it("can filter all language service functionality", () => { + test({ + [atotcFile]: atotcText + }, { + availableExtensions: ["test-service-filters"], + expectedDiagnostics: ["program-diagnostics-replaced", "semantic-diagnostics-replaced", "syntactic-diagnostics-replaced"], + compilerOptions: { + extensions: ["test-service-filters"], + module: ModuleKind.CommonJS, + } + }, languageServiceCompile, testDummyLS); + }); + + it("can filter without altering functionality", () => { + test({ + ["main.ts"]: "console.log('Hello, test.') -" + }, { + availableExtensions: ["test-service-passthru"], + expectedDiagnostics: [2362, 1109], + compilerOptions: { + extensions: ["test-service-passthru"], + module: ModuleKind.CommonJS, + } + }, languageServiceCompile); + }); + + it("can filter and mutate while chaining plugins", () => { + test({ + ["main.ts"]: "console.log('Hello, test.') -" + }, { + availableExtensions: ["test-service-chain"], + expectedDiagnostics: ["program-diagnostics-mutated", "semantic-diagnostics-mutated", "syntactic-diagnostics-mutated", 2362, 1109], + compilerOptions: { + extensions: ["test-service-chain"], + module: ModuleKind.CommonJS, + } + }, languageServiceCompile); + }); + it("can run all lint plugins in the language service", () => { test({ "main.ts": `console.log("Hello, world!");`,