TypeScript/src/services/shims.ts
2021-11-16 16:41:21 -08:00

1359 lines
61 KiB
TypeScript

//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
/* @internal */
let debugObjectHost: { CollectGarbage(): void } = (function (this: any) { // eslint-disable-line prefer-const
return this;
})();
// We need to use 'null' to interface with the managed side.
/* eslint-disable no-in-operator */
/* @internal */
namespace ts {
interface DiscoverTypingsInfo {
fileNames: string[]; // The file names that belong to the same project.
projectRootPath: string; // The path to the project root directory
safeListPath: string; // The path used to retrieve the safe list
packageNameToTypingLocation: ESMap<string, JsTyping.CachedTyping>; // The map of package names to their cached typing locations and installed versions
typeAcquisition: TypeAcquisition; // Used to customize the type acquisition process
compilerOptions: CompilerOptions; // Used as a source for typing inference
unresolvedImports: readonly string[]; // List of unresolved module ids from imports
typesRegistry: ReadonlyESMap<string, MapLike<string>>; // The map of available typings in npm to maps of TS versions to their latest supported versions
}
export interface ScriptSnapshotShim {
/** Gets a portion of the script snapshot specified by [start, end). */
getText(start: number, end: number): string;
/** Gets the length of this script snapshot. */
getLength(): number;
/**
* Returns a JSON-encoded value of the type:
* { span: { start: number; length: number }; newLength: number }
*
* Or undefined value if there was no change.
*/
getChangeRange(oldSnapshot: ScriptSnapshotShim): string | undefined;
/** Releases all resources held by this script snapshot */
dispose?(): void;
}
export interface Logger {
log(s: string): void;
trace(s: string): void;
error(s: string): void;
}
/** Public interface of the host of a language service shim instance. */
export interface LanguageServiceShimHost extends Logger {
getCompilationSettings(): string;
/** Returns a JSON-encoded value of the type: string[] */
getScriptFileNames(): string;
getScriptKind?(fileName: string): ScriptKind;
getScriptVersion(fileName: string): string;
getScriptSnapshot(fileName: string): ScriptSnapshotShim;
getLocalizedDiagnosticMessages(): string;
getCancellationToken(): HostCancellationToken;
getCurrentDirectory(): string;
getDirectories(path: string): string;
getDefaultLibFileName(options: string): string;
getNewLine?(): string;
getProjectVersion?(): string;
useCaseSensitiveFileNames?(): boolean;
getTypeRootsVersion?(): number;
readDirectory(rootDir: string, extension: string, basePaths?: string, excludeEx?: string, includeFileEx?: string, includeDirEx?: string, depth?: number): string;
readFile(path: string, encoding?: string): string | undefined;
fileExists(path: string): boolean;
getModuleResolutionsForFile?(fileName: string): string;
getTypeReferenceDirectiveResolutionsForFile?(fileName: string): string;
directoryExists(directoryName: string): boolean;
}
/** Public interface of the core-services host instance used in managed side */
export interface CoreServicesShimHost extends Logger {
directoryExists(directoryName: string): boolean;
fileExists(fileName: string): boolean;
getCurrentDirectory(): string;
getDirectories(path: string): string;
/**
* Returns a JSON-encoded value of the type: string[]
*
* @param exclude A JSON encoded string[] containing the paths to exclude
* when enumerating the directory.
*/
readDirectory(rootDir: string, extension: string, basePaths?: string, excludeEx?: string, includeFileEx?: string, includeDirEx?: string, depth?: number): string;
/**
* Read arbitrary text files on disk, i.e. when resolution procedure needs the content of 'package.json' to determine location of bundled typings for node modules
*/
readFile(fileName: string): string | undefined;
realpath?(path: string): string;
trace(s: string): void;
useCaseSensitiveFileNames?(): boolean;
}
///
/// Pre-processing
///
// Note: This is being using by the host (VS) and is marshaled back and forth.
// When changing this make sure the changes are reflected in the managed side as well
export interface ShimsFileReference {
path: string;
position: number;
length: number;
}
/** Public interface of a language service instance shim. */
export interface ShimFactory {
registerShim(shim: Shim): void;
unregisterShim(shim: Shim): void;
}
export interface Shim {
dispose(_dummy: {}): void;
}
export interface LanguageServiceShim extends Shim {
languageService: LanguageService;
dispose(_dummy: {}): void;
refresh(throwOnError: boolean): void;
cleanupSemanticCache(): void;
getSyntacticDiagnostics(fileName: string): string;
getSemanticDiagnostics(fileName: string): string;
getSuggestionDiagnostics(fileName: string): string;
getCompilerOptionsDiagnostics(): string;
getSyntacticClassifications(fileName: string, start: number, length: number): string;
getSemanticClassifications(fileName: string, start: number, length: number, format?: SemanticClassificationFormat): string;
getEncodedSyntacticClassifications(fileName: string, start: number, length: number): string;
getEncodedSemanticClassifications(fileName: string, start: number, length: number, format?: SemanticClassificationFormat): string;
getCompletionsAtPosition(fileName: string, position: number, preferences: UserPreferences | undefined, formattingSettings: FormatCodeSettings | undefined): string;
getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: string/*Services.FormatCodeOptions*/ | undefined, source: string | undefined, preferences: UserPreferences | undefined, data: CompletionEntryData | undefined): string;
getQuickInfoAtPosition(fileName: string, position: number): string;
getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): string;
getBreakpointStatementAtPosition(fileName: string, position: number): string;
getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): string;
/**
* Returns a JSON-encoded value of the type:
* { canRename: boolean, localizedErrorMessage: string, displayName: string, fullDisplayName: string, kind: string, kindModifiers: string, triggerSpan: { start; length } }
*/
getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): string;
getSmartSelectionRange(fileName: string, position: number): string;
/**
* Returns a JSON-encoded value of the type:
* { fileName: string, textSpan: { start: number, length: number } }[]
*/
findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): string;
/**
* Returns a JSON-encoded value of the type:
* { fileName: string; textSpan: { start: number; length: number}; kind: string; name: string; containerKind: string; containerName: string }
*
* Or undefined value if no definition can be found.
*/
getDefinitionAtPosition(fileName: string, position: number): string;
getDefinitionAndBoundSpan(fileName: string, position: number): string;
/**
* Returns a JSON-encoded value of the type:
* { fileName: string; textSpan: { start: number; length: number}; kind: string; name: string; containerKind: string; containerName: string }
*
* Or undefined value if no definition can be found.
*/
getTypeDefinitionAtPosition(fileName: string, position: number): string;
/**
* Returns a JSON-encoded value of the type:
* { fileName: string; textSpan: { start: number; length: number}; }[]
*/
getImplementationAtPosition(fileName: string, position: number): string;
/**
* Returns a JSON-encoded value of the type:
* { fileName: string; textSpan: { start: number; length: number}; isWriteAccess: boolean, isDefinition?: boolean }[]
*/
getReferencesAtPosition(fileName: string, position: number): string;
/**
* Returns a JSON-encoded value of the type:
* { definition: <encoded>; references: <encoded>[] }[]
*/
findReferences(fileName: string, position: number): string;
/**
* Returns a JSON-encoded value of the type:
* { fileName: string; textSpan: { start: number; length: number}; isWriteAccess: boolean, isDefinition?: boolean }[]
*/
getFileReferences(fileName: string): string;
/**
* @deprecated
* Returns a JSON-encoded value of the type:
* { fileName: string; textSpan: { start: number; length: number}; isWriteAccess: boolean }[]
*/
getOccurrencesAtPosition(fileName: string, position: number): string;
/**
* Returns a JSON-encoded value of the type:
* { fileName: string; highlights: { start: number; length: number, isDefinition: boolean }[] }[]
*
* @param fileToSearch A JSON encoded string[] containing the file names that should be
* considered when searching.
*/
getDocumentHighlights(fileName: string, position: number, filesToSearch: string): string;
/**
* Returns a JSON-encoded value of the type:
* { name: string; kind: string; kindModifiers: string; containerName: string; containerKind: string; matchKind: string; fileName: string; textSpan: { start: number; length: number}; } [] = [];
*/
getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string): string;
/**
* Returns a JSON-encoded value of the type:
* { text: string; kind: string; kindModifiers: string; bolded: boolean; grayed: boolean; indent: number; spans: { start: number; length: number; }[]; childItems: <recursive use of this type>[] } [] = [];
*/
getNavigationBarItems(fileName: string): string;
/** Returns a JSON-encoded value of the type ts.NavigationTree. */
getNavigationTree(fileName: string): string;
/**
* Returns a JSON-encoded value of the type:
* { textSpan: { start: number, length: number }; hintSpan: { start: number, length: number }; bannerText: string; autoCollapse: boolean } [] = [];
*/
getOutliningSpans(fileName: string): string;
getTodoComments(fileName: string, todoCommentDescriptors: string): string;
getBraceMatchingAtPosition(fileName: string, position: number): string;
getIndentationAtPosition(fileName: string, position: number, options: string/*Services.EditorOptions*/): string;
getFormattingEditsForRange(fileName: string, start: number, end: number, options: string/*Services.FormatCodeOptions*/): string;
getFormattingEditsForDocument(fileName: string, options: string/*Services.FormatCodeOptions*/): string;
getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: string/*Services.FormatCodeOptions*/): string;
/**
* Returns JSON-encoded value of the type TextInsertion.
*/
getDocCommentTemplateAtPosition(fileName: string, position: number, options?: DocCommentTemplateOptions): string;
/**
* Returns JSON-encoded boolean to indicate whether we should support brace location
* at the current position.
* E.g. we don't want brace completion inside string-literals, comments, etc.
*/
isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): string;
/**
* Returns a JSON-encoded TextSpan | undefined indicating the range of the enclosing comment, if it exists.
*/
getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): string;
prepareCallHierarchy(fileName: string, position: number): string;
provideCallHierarchyIncomingCalls(fileName: string, position: number): string;
provideCallHierarchyOutgoingCalls(fileName: string, position: number): string;
provideInlayHints(fileName: string, span: TextSpan, preference: InlayHintsOptions | undefined): string;
getEmitOutput(fileName: string): string;
getEmitOutputObject(fileName: string): EmitOutput;
toggleLineComment(fileName: string, textChange: TextRange): string;
toggleMultilineComment(fileName: string, textChange: TextRange): string;
commentSelection(fileName: string, textChange: TextRange): string;
uncommentSelection(fileName: string, textChange: TextRange): string;
}
export interface ClassifierShim extends Shim {
getEncodedLexicalClassifications(text: string, lexState: EndOfLineState, syntacticClassifierAbsent?: boolean): string;
getClassificationsForLine(text: string, lexState: EndOfLineState, syntacticClassifierAbsent?: boolean): string;
}
export interface CoreServicesShim extends Shim {
getAutomaticTypeDirectiveNames(compilerOptionsJson: string): string;
getPreProcessedFileInfo(fileName: string, sourceText: IScriptSnapshot): string;
getTSConfigFileInfo(fileName: string, sourceText: IScriptSnapshot): string;
getDefaultCompilationSettings(): string;
discoverTypings(discoverTypingsJson: string): string;
}
function logInternalError(logger: Logger, err: Error) {
if (logger) {
logger.log("*INTERNAL ERROR* - Exception in typescript services: " + err.message);
}
}
class ScriptSnapshotShimAdapter implements IScriptSnapshot {
constructor(private scriptSnapshotShim: ScriptSnapshotShim) {
}
public getText(start: number, end: number): string {
return this.scriptSnapshotShim.getText(start, end);
}
public getLength(): number {
return this.scriptSnapshotShim.getLength();
}
public getChangeRange(oldSnapshot: IScriptSnapshot): TextChangeRange | undefined {
const oldSnapshotShim = oldSnapshot as ScriptSnapshotShimAdapter;
const encoded = this.scriptSnapshotShim.getChangeRange(oldSnapshotShim.scriptSnapshotShim);
/* eslint-disable no-null/no-null */
if (encoded === null) {
return null!; // TODO: GH#18217
}
/* eslint-enable no-null/no-null */
const decoded: { span: { start: number; length: number; }; newLength: number; } = JSON.parse(encoded!); // TODO: GH#18217
return createTextChangeRange(
createTextSpan(decoded.span.start, decoded.span.length), decoded.newLength);
}
public dispose(): void {
// if scriptSnapshotShim is a COM object then property check becomes method call with no arguments
// 'in' does not have this effect
if ("dispose" in this.scriptSnapshotShim) {
this.scriptSnapshotShim.dispose!(); // TODO: GH#18217 Can we just use `if (this.scriptSnapshotShim.dispose)`?
}
}
}
export class LanguageServiceShimHostAdapter implements LanguageServiceHost {
private loggingEnabled = false;
private tracingEnabled = false;
public resolveModuleNames: ((moduleName: string[], containingFile: string) => (ResolvedModuleFull | undefined)[]) | undefined;
public resolveTypeReferenceDirectives: ((typeDirectiveNames: string[], containingFile: string) => (ResolvedTypeReferenceDirective | undefined)[]) | undefined;
public directoryExists: ((directoryName: string) => boolean) | undefined;
constructor(private shimHost: LanguageServiceShimHost) {
// if shimHost is a COM object then property check will become method call with no arguments.
// 'in' does not have this effect.
if ("getModuleResolutionsForFile" in this.shimHost) {
this.resolveModuleNames = (moduleNames, containingFile) => {
const resolutionsInFile = JSON.parse(this.shimHost.getModuleResolutionsForFile!(containingFile)) as MapLike<string>; // TODO: GH#18217
return map(moduleNames, name => {
const result = getProperty(resolutionsInFile, name);
return result ? { resolvedFileName: result, extension: extensionFromPath(result), isExternalLibraryImport: false } : undefined;
});
};
}
if ("directoryExists" in this.shimHost) {
this.directoryExists = directoryName => this.shimHost.directoryExists(directoryName);
}
if ("getTypeReferenceDirectiveResolutionsForFile" in this.shimHost) {
this.resolveTypeReferenceDirectives = (typeDirectiveNames, containingFile) => {
const typeDirectivesForFile = JSON.parse(this.shimHost.getTypeReferenceDirectiveResolutionsForFile!(containingFile)) as MapLike<ResolvedTypeReferenceDirective>; // TODO: GH#18217
return map(typeDirectiveNames, name => getProperty(typeDirectivesForFile, name));
};
}
}
public log(s: string): void {
if (this.loggingEnabled) {
this.shimHost.log(s);
}
}
public trace(s: string): void {
if (this.tracingEnabled) {
this.shimHost.trace(s);
}
}
public error(s: string): void {
this.shimHost.error(s);
}
public getProjectVersion(): string {
if (!this.shimHost.getProjectVersion) {
// shimmed host does not support getProjectVersion
return undefined!; // TODO: GH#18217
}
return this.shimHost.getProjectVersion();
}
public getTypeRootsVersion(): number {
if (!this.shimHost.getTypeRootsVersion) {
return 0;
}
return this.shimHost.getTypeRootsVersion();
}
public useCaseSensitiveFileNames(): boolean {
return this.shimHost.useCaseSensitiveFileNames ? this.shimHost.useCaseSensitiveFileNames() : false;
}
public getCompilationSettings(): CompilerOptions {
const settingsJson = this.shimHost.getCompilationSettings();
// eslint-disable-next-line no-null/no-null
if (settingsJson === null || settingsJson === "") {
throw Error("LanguageServiceShimHostAdapter.getCompilationSettings: empty compilationSettings");
}
const compilerOptions = JSON.parse(settingsJson) as CompilerOptions;
// permit language service to handle all files (filtering should be performed on the host side)
compilerOptions.allowNonTsExtensions = true;
return compilerOptions;
}
public getScriptFileNames(): string[] {
const encoded = this.shimHost.getScriptFileNames();
return JSON.parse(encoded);
}
public getScriptSnapshot(fileName: string): IScriptSnapshot | undefined {
const scriptSnapshot = this.shimHost.getScriptSnapshot(fileName);
return scriptSnapshot && new ScriptSnapshotShimAdapter(scriptSnapshot);
}
public getScriptKind(fileName: string): ScriptKind {
if ("getScriptKind" in this.shimHost) {
return this.shimHost.getScriptKind!(fileName); // TODO: GH#18217
}
else {
return ScriptKind.Unknown;
}
}
public getScriptVersion(fileName: string): string {
return this.shimHost.getScriptVersion(fileName);
}
public getLocalizedDiagnosticMessages() {
/* eslint-disable no-null/no-null */
const diagnosticMessagesJson = this.shimHost.getLocalizedDiagnosticMessages();
if (diagnosticMessagesJson === null || diagnosticMessagesJson === "") {
return null;
}
try {
return JSON.parse(diagnosticMessagesJson);
}
catch (e) {
this.log(e.description || "diagnosticMessages.generated.json has invalid JSON format");
return null;
}
/* eslint-enable no-null/no-null */
}
public getCancellationToken(): HostCancellationToken {
const hostCancellationToken = this.shimHost.getCancellationToken();
return new ThrottledCancellationToken(hostCancellationToken);
}
public getCurrentDirectory(): string {
return this.shimHost.getCurrentDirectory();
}
public getDirectories(path: string): string[] {
return JSON.parse(this.shimHost.getDirectories(path));
}
public getDefaultLibFileName(options: CompilerOptions): string {
return this.shimHost.getDefaultLibFileName(JSON.stringify(options));
}
public readDirectory(path: string, extensions?: readonly string[], exclude?: string[], include?: string[], depth?: number): string[] {
const pattern = getFileMatcherPatterns(path, exclude, include,
this.shimHost.useCaseSensitiveFileNames!(), this.shimHost.getCurrentDirectory()); // TODO: GH#18217
return JSON.parse(this.shimHost.readDirectory(
path,
JSON.stringify(extensions),
JSON.stringify(pattern.basePaths),
pattern.excludePattern,
pattern.includeFilePattern,
pattern.includeDirectoryPattern,
depth
));
}
public readFile(path: string, encoding?: string): string | undefined {
return this.shimHost.readFile(path, encoding);
}
public fileExists(path: string): boolean {
return this.shimHost.fileExists(path);
}
}
export class CoreServicesShimHostAdapter implements ParseConfigHost, ModuleResolutionHost, JsTyping.TypingResolutionHost {
public directoryExists: (directoryName: string) => boolean;
public realpath: (path: string) => string;
public useCaseSensitiveFileNames: boolean;
constructor(private shimHost: CoreServicesShimHost) {
this.useCaseSensitiveFileNames = this.shimHost.useCaseSensitiveFileNames ? this.shimHost.useCaseSensitiveFileNames() : false;
if ("directoryExists" in this.shimHost) {
this.directoryExists = directoryName => this.shimHost.directoryExists(directoryName);
}
else {
this.directoryExists = undefined!; // TODO: GH#18217
}
if ("realpath" in this.shimHost) {
this.realpath = path => this.shimHost.realpath!(path); // TODO: GH#18217
}
else {
this.realpath = undefined!; // TODO: GH#18217
}
}
public readDirectory(rootDir: string, extensions: readonly string[], exclude: readonly string[], include: readonly string[], depth?: number): string[] {
const pattern = getFileMatcherPatterns(rootDir, exclude, include,
this.shimHost.useCaseSensitiveFileNames!(), this.shimHost.getCurrentDirectory()); // TODO: GH#18217
return JSON.parse(this.shimHost.readDirectory(
rootDir,
JSON.stringify(extensions),
JSON.stringify(pattern.basePaths),
pattern.excludePattern,
pattern.includeFilePattern,
pattern.includeDirectoryPattern,
depth
));
}
public fileExists(fileName: string): boolean {
return this.shimHost.fileExists(fileName);
}
public readFile(fileName: string): string | undefined {
return this.shimHost.readFile(fileName);
}
public getDirectories(path: string): string[] {
return JSON.parse(this.shimHost.getDirectories(path));
}
}
function simpleForwardCall(logger: Logger, actionDescription: string, action: () => {}, logPerformance: boolean): {} {
let start: number | undefined;
if (logPerformance) {
logger.log(actionDescription);
start = timestamp();
}
const result = action();
if (logPerformance) {
const end = timestamp();
logger.log(`${actionDescription} completed in ${end - start!} msec`);
if (isString(result)) {
let str = result;
if (str.length > 128) {
str = str.substring(0, 128) + "...";
}
logger.log(` result.length=${str.length}, result='${JSON.stringify(str)}'`);
}
}
return result;
}
function forwardJSONCall(logger: Logger, actionDescription: string, action: () => {} | null | undefined, logPerformance: boolean): string {
return forwardCall(logger, actionDescription, /*returnJson*/ true, action, logPerformance) as string;
}
function forwardCall<T>(logger: Logger, actionDescription: string, returnJson: boolean, action: () => T, logPerformance: boolean): T | string {
try {
const result = simpleForwardCall(logger, actionDescription, action, logPerformance);
return returnJson ? JSON.stringify({ result }) : result as T;
}
catch (err) {
if (err instanceof OperationCanceledException) {
return JSON.stringify({ canceled: true });
}
logInternalError(logger, err);
err.description = actionDescription;
return JSON.stringify({ error: err });
}
}
class ShimBase implements Shim {
constructor(private factory: ShimFactory) {
factory.registerShim(this);
}
public dispose(_dummy: {}): void {
this.factory.unregisterShim(this);
}
}
export interface RealizedDiagnostic {
message: string;
start: number;
length: number;
category: string;
code: number;
reportsUnnecessary?: {};
reportsDeprecated?: {};
}
export function realizeDiagnostics(diagnostics: readonly Diagnostic[], newLine: string): RealizedDiagnostic[] {
return diagnostics.map(d => realizeDiagnostic(d, newLine));
}
function realizeDiagnostic(diagnostic: Diagnostic, newLine: string): RealizedDiagnostic {
return {
message: flattenDiagnosticMessageText(diagnostic.messageText, newLine),
start: diagnostic.start!, // TODO: GH#18217
length: diagnostic.length!, // TODO: GH#18217
category: diagnosticCategoryName(diagnostic),
code: diagnostic.code,
reportsUnnecessary: diagnostic.reportsUnnecessary,
reportsDeprecated: diagnostic.reportsDeprecated
};
}
class LanguageServiceShimObject extends ShimBase implements LanguageServiceShim {
private logger: Logger;
private logPerformance = false;
constructor(factory: ShimFactory,
private host: LanguageServiceShimHost,
public languageService: LanguageService) {
super(factory);
this.logger = this.host;
}
public forwardJSONCall(actionDescription: string, action: () => {} | null | undefined): string {
return forwardJSONCall(this.logger, actionDescription, action, this.logPerformance);
}
/// DISPOSE
/**
* Ensure (almost) deterministic release of internal Javascript resources when
* some external native objects holds onto us (e.g. Com/Interop).
*/
public dispose(dummy: {}): void {
this.logger.log("dispose()");
this.languageService.dispose();
this.languageService = null!; // eslint-disable-line no-null/no-null
// force a GC
if (debugObjectHost && debugObjectHost.CollectGarbage) {
debugObjectHost.CollectGarbage();
this.logger.log("CollectGarbage()");
}
this.logger = null!; // eslint-disable-line no-null/no-null
super.dispose(dummy);
}
/// REFRESH
/**
* Update the list of scripts known to the compiler
*/
public refresh(throwOnError: boolean): void {
this.forwardJSONCall(
`refresh(${throwOnError})`,
() => null // eslint-disable-line no-null/no-null
);
}
public cleanupSemanticCache(): void {
this.forwardJSONCall(
"cleanupSemanticCache()",
() => {
this.languageService.cleanupSemanticCache();
return null; // eslint-disable-line no-null/no-null
});
}
private realizeDiagnostics(diagnostics: readonly Diagnostic[]): { message: string; start: number; length: number; category: string; }[] {
const newLine = getNewLineOrDefaultFromHost(this.host);
return realizeDiagnostics(diagnostics, newLine);
}
public getSyntacticClassifications(fileName: string, start: number, length: number): string {
return this.forwardJSONCall(
`getSyntacticClassifications('${fileName}', ${start}, ${length})`,
() => this.languageService.getSyntacticClassifications(fileName, createTextSpan(start, length))
);
}
public getSemanticClassifications(fileName: string, start: number, length: number): string {
return this.forwardJSONCall(
`getSemanticClassifications('${fileName}', ${start}, ${length})`,
() => this.languageService.getSemanticClassifications(fileName, createTextSpan(start, length))
);
}
public getEncodedSyntacticClassifications(fileName: string, start: number, length: number): string {
return this.forwardJSONCall(
`getEncodedSyntacticClassifications('${fileName}', ${start}, ${length})`,
// directly serialize the spans out to a string. This is much faster to decode
// on the managed side versus a full JSON array.
() => convertClassifications(this.languageService.getEncodedSyntacticClassifications(fileName, createTextSpan(start, length)))
);
}
public getEncodedSemanticClassifications(fileName: string, start: number, length: number): string {
return this.forwardJSONCall(
`getEncodedSemanticClassifications('${fileName}', ${start}, ${length})`,
// directly serialize the spans out to a string. This is much faster to decode
// on the managed side versus a full JSON array.
() => convertClassifications(this.languageService.getEncodedSemanticClassifications(fileName, createTextSpan(start, length)))
);
}
public getSyntacticDiagnostics(fileName: string): string {
return this.forwardJSONCall(
`getSyntacticDiagnostics('${fileName}')`,
() => {
const diagnostics = this.languageService.getSyntacticDiagnostics(fileName);
return this.realizeDiagnostics(diagnostics);
});
}
public getSemanticDiagnostics(fileName: string): string {
return this.forwardJSONCall(
`getSemanticDiagnostics('${fileName}')`,
() => {
const diagnostics = this.languageService.getSemanticDiagnostics(fileName);
return this.realizeDiagnostics(diagnostics);
});
}
public getSuggestionDiagnostics(fileName: string): string {
return this.forwardJSONCall(`getSuggestionDiagnostics('${fileName}')`, () => this.realizeDiagnostics(this.languageService.getSuggestionDiagnostics(fileName)));
}
public getCompilerOptionsDiagnostics(): string {
return this.forwardJSONCall(
"getCompilerOptionsDiagnostics()",
() => {
const diagnostics = this.languageService.getCompilerOptionsDiagnostics();
return this.realizeDiagnostics(diagnostics);
});
}
/// QUICKINFO
/**
* Computes a string representation of the type at the requested position
* in the active file.
*/
public getQuickInfoAtPosition(fileName: string, position: number): string {
return this.forwardJSONCall(
`getQuickInfoAtPosition('${fileName}', ${position})`,
() => this.languageService.getQuickInfoAtPosition(fileName, position)
);
}
/// NAMEORDOTTEDNAMESPAN
/**
* Computes span information of the name or dotted name at the requested position
* in the active file.
*/
public getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): string {
return this.forwardJSONCall(
`getNameOrDottedNameSpan('${fileName}', ${startPos}, ${endPos})`,
() => this.languageService.getNameOrDottedNameSpan(fileName, startPos, endPos)
);
}
/**
* STATEMENTSPAN
* Computes span information of statement at the requested position in the active file.
*/
public getBreakpointStatementAtPosition(fileName: string, position: number): string {
return this.forwardJSONCall(
`getBreakpointStatementAtPosition('${fileName}', ${position})`,
() => this.languageService.getBreakpointStatementAtPosition(fileName, position)
);
}
/// SIGNATUREHELP
public getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): string {
return this.forwardJSONCall(
`getSignatureHelpItems('${fileName}', ${position})`,
() => this.languageService.getSignatureHelpItems(fileName, position, options)
);
}
/// GOTO DEFINITION
/**
* Computes the definition location and file for the symbol
* at the requested position.
*/
public getDefinitionAtPosition(fileName: string, position: number): string {
return this.forwardJSONCall(
`getDefinitionAtPosition('${fileName}', ${position})`,
() => this.languageService.getDefinitionAtPosition(fileName, position)
);
}
/**
* Computes the definition location and file for the symbol
* at the requested position.
*/
public getDefinitionAndBoundSpan(fileName: string, position: number): string {
return this.forwardJSONCall(
`getDefinitionAndBoundSpan('${fileName}', ${position})`,
() => this.languageService.getDefinitionAndBoundSpan(fileName, position)
);
}
/// GOTO Type
/**
* Computes the definition location of the type of the symbol
* at the requested position.
*/
public getTypeDefinitionAtPosition(fileName: string, position: number): string {
return this.forwardJSONCall(
`getTypeDefinitionAtPosition('${fileName}', ${position})`,
() => this.languageService.getTypeDefinitionAtPosition(fileName, position)
);
}
/// GOTO Implementation
/**
* Computes the implementation location of the symbol
* at the requested position.
*/
public getImplementationAtPosition(fileName: string, position: number): string {
return this.forwardJSONCall(
`getImplementationAtPosition('${fileName}', ${position})`,
() => this.languageService.getImplementationAtPosition(fileName, position)
);
}
public getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): string {
return this.forwardJSONCall(
`getRenameInfo('${fileName}', ${position})`,
() => this.languageService.getRenameInfo(fileName, position, options)
);
}
public getSmartSelectionRange(fileName: string, position: number): string {
return this.forwardJSONCall(
`getSmartSelectionRange('${fileName}', ${position})`,
() => this.languageService.getSmartSelectionRange(fileName, position)
);
}
public findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): string {
return this.forwardJSONCall(
`findRenameLocations('${fileName}', ${position}, ${findInStrings}, ${findInComments}, ${providePrefixAndSuffixTextForRename})`,
() => this.languageService.findRenameLocations(fileName, position, findInStrings, findInComments, providePrefixAndSuffixTextForRename)
);
}
/// GET BRACE MATCHING
public getBraceMatchingAtPosition(fileName: string, position: number): string {
return this.forwardJSONCall(
`getBraceMatchingAtPosition('${fileName}', ${position})`,
() => this.languageService.getBraceMatchingAtPosition(fileName, position)
);
}
public isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): string {
return this.forwardJSONCall(
`isValidBraceCompletionAtPosition('${fileName}', ${position}, ${openingBrace})`,
() => this.languageService.isValidBraceCompletionAtPosition(fileName, position, openingBrace)
);
}
public getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): string {
return this.forwardJSONCall(
`getSpanOfEnclosingComment('${fileName}', ${position})`,
() => this.languageService.getSpanOfEnclosingComment(fileName, position, onlyMultiLine)
);
}
/// GET SMART INDENT
public getIndentationAtPosition(fileName: string, position: number, options: string /*Services.EditorOptions*/): string {
return this.forwardJSONCall(
`getIndentationAtPosition('${fileName}', ${position})`,
() => {
const localOptions: EditorOptions = JSON.parse(options);
return this.languageService.getIndentationAtPosition(fileName, position, localOptions);
});
}
/// GET REFERENCES
public getReferencesAtPosition(fileName: string, position: number): string {
return this.forwardJSONCall(
`getReferencesAtPosition('${fileName}', ${position})`,
() => this.languageService.getReferencesAtPosition(fileName, position)
);
}
public findReferences(fileName: string, position: number): string {
return this.forwardJSONCall(
`findReferences('${fileName}', ${position})`,
() => this.languageService.findReferences(fileName, position)
);
}
public getFileReferences(fileName: string) {
return this.forwardJSONCall(
`getFileReferences('${fileName})`,
() => this.languageService.getFileReferences(fileName)
);
}
public getOccurrencesAtPosition(fileName: string, position: number): string {
return this.forwardJSONCall(
`getOccurrencesAtPosition('${fileName}', ${position})`,
() => this.languageService.getOccurrencesAtPosition(fileName, position)
);
}
public getDocumentHighlights(fileName: string, position: number, filesToSearch: string): string {
return this.forwardJSONCall(
`getDocumentHighlights('${fileName}', ${position})`,
() => {
const results = this.languageService.getDocumentHighlights(fileName, position, JSON.parse(filesToSearch));
// workaround for VS document highlighting issue - keep only items from the initial file
const normalizedName = toFileNameLowerCase(normalizeSlashes(fileName));
return filter(results, r => toFileNameLowerCase(normalizeSlashes(r.fileName)) === normalizedName);
});
}
/// COMPLETION LISTS
/**
* Get a string based representation of the completions
* to provide at the given source position and providing a member completion
* list if requested.
*/
public getCompletionsAtPosition(fileName: string, position: number, preferences: GetCompletionsAtPositionOptions | undefined, formattingSettings: FormatCodeSettings | undefined) {
return this.forwardJSONCall(
`getCompletionsAtPosition('${fileName}', ${position}, ${preferences}, ${formattingSettings})`,
() => this.languageService.getCompletionsAtPosition(fileName, position, preferences, formattingSettings)
);
}
/** Get a string based representation of a completion list entry details */
public getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: string/*Services.FormatCodeOptions*/ | undefined, source: string | undefined, preferences: UserPreferences | undefined, data: CompletionEntryData | undefined) {
return this.forwardJSONCall(
`getCompletionEntryDetails('${fileName}', ${position}, '${entryName}')`,
() => {
const localOptions: FormatCodeOptions = formatOptions === undefined ? undefined : JSON.parse(formatOptions);
return this.languageService.getCompletionEntryDetails(fileName, position, entryName, localOptions, source, preferences, data);
}
);
}
public getFormattingEditsForRange(fileName: string, start: number, end: number, options: string/*Services.FormatCodeOptions*/): string {
return this.forwardJSONCall(
`getFormattingEditsForRange('${fileName}', ${start}, ${end})`,
() => {
const localOptions: FormatCodeOptions = JSON.parse(options);
return this.languageService.getFormattingEditsForRange(fileName, start, end, localOptions);
});
}
public getFormattingEditsForDocument(fileName: string, options: string/*Services.FormatCodeOptions*/): string {
return this.forwardJSONCall(
`getFormattingEditsForDocument('${fileName}')`,
() => {
const localOptions: FormatCodeOptions = JSON.parse(options);
return this.languageService.getFormattingEditsForDocument(fileName, localOptions);
});
}
public getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: string/*Services.FormatCodeOptions*/): string {
return this.forwardJSONCall(
`getFormattingEditsAfterKeystroke('${fileName}', ${position}, '${key}')`,
() => {
const localOptions: FormatCodeOptions = JSON.parse(options);
return this.languageService.getFormattingEditsAfterKeystroke(fileName, position, key, localOptions);
});
}
public getDocCommentTemplateAtPosition(fileName: string, position: number, options?: DocCommentTemplateOptions): string {
return this.forwardJSONCall(
`getDocCommentTemplateAtPosition('${fileName}', ${position})`,
() => this.languageService.getDocCommentTemplateAtPosition(fileName, position, options)
);
}
/// NAVIGATE TO
/** Return a list of symbols that are interesting to navigate to */
public getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string): string {
return this.forwardJSONCall(
`getNavigateToItems('${searchValue}', ${maxResultCount}, ${fileName})`,
() => this.languageService.getNavigateToItems(searchValue, maxResultCount, fileName)
);
}
public getNavigationBarItems(fileName: string): string {
return this.forwardJSONCall(
`getNavigationBarItems('${fileName}')`,
() => this.languageService.getNavigationBarItems(fileName)
);
}
public getNavigationTree(fileName: string): string {
return this.forwardJSONCall(
`getNavigationTree('${fileName}')`,
() => this.languageService.getNavigationTree(fileName)
);
}
public getOutliningSpans(fileName: string): string {
return this.forwardJSONCall(
`getOutliningSpans('${fileName}')`,
() => this.languageService.getOutliningSpans(fileName)
);
}
public getTodoComments(fileName: string, descriptors: string): string {
return this.forwardJSONCall(
`getTodoComments('${fileName}')`,
() => this.languageService.getTodoComments(fileName, JSON.parse(descriptors))
);
}
/// CALL HIERARCHY
public prepareCallHierarchy(fileName: string, position: number): string {
return this.forwardJSONCall(
`prepareCallHierarchy('${fileName}', ${position})`,
() => this.languageService.prepareCallHierarchy(fileName, position)
);
}
public provideCallHierarchyIncomingCalls(fileName: string, position: number): string {
return this.forwardJSONCall(
`provideCallHierarchyIncomingCalls('${fileName}', ${position})`,
() => this.languageService.provideCallHierarchyIncomingCalls(fileName, position)
);
}
public provideCallHierarchyOutgoingCalls(fileName: string, position: number): string {
return this.forwardJSONCall(
`provideCallHierarchyOutgoingCalls('${fileName}', ${position})`,
() => this.languageService.provideCallHierarchyOutgoingCalls(fileName, position)
);
}
public provideInlayHints(fileName: string, span: TextSpan, preference: InlayHintsOptions | undefined): string {
return this.forwardJSONCall(
`provideInlayHints('${fileName}', '${JSON.stringify(span)}', ${JSON.stringify(preference)})`,
() => this.languageService.provideInlayHints(fileName, span, preference)
);
}
/// Emit
public getEmitOutput(fileName: string): string {
return this.forwardJSONCall(
`getEmitOutput('${fileName}')`,
() => {
const { diagnostics, ...rest } = this.languageService.getEmitOutput(fileName);
return { ...rest, diagnostics: this.realizeDiagnostics(diagnostics) };
}
);
}
public getEmitOutputObject(fileName: string): EmitOutput {
return forwardCall(
this.logger,
`getEmitOutput('${fileName}')`,
/*returnJson*/ false,
() => this.languageService.getEmitOutput(fileName),
this.logPerformance) as EmitOutput;
}
public toggleLineComment(fileName: string, textRange: TextRange): string {
return this.forwardJSONCall(
`toggleLineComment('${fileName}', '${JSON.stringify(textRange)}')`,
() => this.languageService.toggleLineComment(fileName, textRange)
);
}
public toggleMultilineComment(fileName: string, textRange: TextRange): string {
return this.forwardJSONCall(
`toggleMultilineComment('${fileName}', '${JSON.stringify(textRange)}')`,
() => this.languageService.toggleMultilineComment(fileName, textRange)
);
}
public commentSelection(fileName: string, textRange: TextRange): string {
return this.forwardJSONCall(
`commentSelection('${fileName}', '${JSON.stringify(textRange)}')`,
() => this.languageService.commentSelection(fileName, textRange)
);
}
public uncommentSelection(fileName: string, textRange: TextRange): string {
return this.forwardJSONCall(
`uncommentSelection('${fileName}', '${JSON.stringify(textRange)}')`,
() => this.languageService.uncommentSelection(fileName, textRange)
);
}
}
function convertClassifications(classifications: Classifications): { spans: string, endOfLineState: EndOfLineState } {
return { spans: classifications.spans.join(","), endOfLineState: classifications.endOfLineState };
}
class ClassifierShimObject extends ShimBase implements ClassifierShim {
public classifier: Classifier;
private logPerformance = false;
constructor(factory: ShimFactory, private logger: Logger) {
super(factory);
this.classifier = createClassifier();
}
public getEncodedLexicalClassifications(text: string, lexState: EndOfLineState, syntacticClassifierAbsent = false): string {
return forwardJSONCall(this.logger, "getEncodedLexicalClassifications",
() => convertClassifications(this.classifier.getEncodedLexicalClassifications(text, lexState, syntacticClassifierAbsent)),
this.logPerformance);
}
/// COLORIZATION
public getClassificationsForLine(text: string, lexState: EndOfLineState, classifyKeywordsInGenerics = false): string {
const classification = this.classifier.getClassificationsForLine(text, lexState, classifyKeywordsInGenerics);
let result = "";
for (const item of classification.entries) {
result += item.length + "\n";
result += item.classification + "\n";
}
result += classification.finalLexState;
return result;
}
}
class CoreServicesShimObject extends ShimBase implements CoreServicesShim {
private logPerformance = false;
private safeList: JsTyping.SafeList | undefined;
constructor(factory: ShimFactory, public readonly logger: Logger, private readonly host: CoreServicesShimHostAdapter) {
super(factory);
}
private forwardJSONCall(actionDescription: string, action: () => {}): string {
return forwardJSONCall(this.logger, actionDescription, action, this.logPerformance);
}
public resolveModuleName(fileName: string, moduleName: string, compilerOptionsJson: string): string {
return this.forwardJSONCall(`resolveModuleName('${fileName}')`, () => {
const compilerOptions = JSON.parse(compilerOptionsJson) as CompilerOptions;
const result = resolveModuleName(moduleName, normalizeSlashes(fileName), compilerOptions, this.host);
let resolvedFileName = result.resolvedModule ? result.resolvedModule.resolvedFileName : undefined;
if (result.resolvedModule && result.resolvedModule.extension !== Extension.Ts && result.resolvedModule.extension !== Extension.Tsx && result.resolvedModule.extension !== Extension.Dts) {
resolvedFileName = undefined;
}
return {
resolvedFileName,
failedLookupLocations: result.failedLookupLocations
};
});
}
public resolveTypeReferenceDirective(fileName: string, typeReferenceDirective: string, compilerOptionsJson: string): string {
return this.forwardJSONCall(`resolveTypeReferenceDirective(${fileName})`, () => {
const compilerOptions = JSON.parse(compilerOptionsJson) as CompilerOptions;
const result = resolveTypeReferenceDirective(typeReferenceDirective, normalizeSlashes(fileName), compilerOptions, this.host);
return {
resolvedFileName: result.resolvedTypeReferenceDirective ? result.resolvedTypeReferenceDirective.resolvedFileName : undefined,
primary: result.resolvedTypeReferenceDirective ? result.resolvedTypeReferenceDirective.primary : true,
failedLookupLocations: result.failedLookupLocations
};
});
}
public getPreProcessedFileInfo(fileName: string, sourceTextSnapshot: IScriptSnapshot): string {
return this.forwardJSONCall(
`getPreProcessedFileInfo('${fileName}')`,
() => {
// for now treat files as JavaScript
const result = preProcessFile(getSnapshotText(sourceTextSnapshot), /* readImportFiles */ true, /* detectJavaScriptImports */ true);
return {
referencedFiles: this.convertFileReferences(result.referencedFiles),
importedFiles: this.convertFileReferences(result.importedFiles),
ambientExternalModules: result.ambientExternalModules,
isLibFile: result.isLibFile,
typeReferenceDirectives: this.convertFileReferences(result.typeReferenceDirectives),
libReferenceDirectives: this.convertFileReferences(result.libReferenceDirectives)
};
});
}
public getAutomaticTypeDirectiveNames(compilerOptionsJson: string): string {
return this.forwardJSONCall(
`getAutomaticTypeDirectiveNames('${compilerOptionsJson}')`,
() => {
const compilerOptions = JSON.parse(compilerOptionsJson) as CompilerOptions;
return getAutomaticTypeDirectiveNames(compilerOptions, this.host);
}
);
}
private convertFileReferences(refs: FileReference[]): ShimsFileReference[] | undefined {
if (!refs) {
return undefined;
}
const result: ShimsFileReference[] = [];
for (const ref of refs) {
result.push({
path: normalizeSlashes(ref.fileName),
position: ref.pos,
length: ref.end - ref.pos
});
}
return result;
}
public getTSConfigFileInfo(fileName: string, sourceTextSnapshot: IScriptSnapshot): string {
return this.forwardJSONCall(
`getTSConfigFileInfo('${fileName}')`,
() => {
const result = parseJsonText(fileName, getSnapshotText(sourceTextSnapshot));
const normalizedFileName = normalizeSlashes(fileName);
const configFile = parseJsonSourceFileConfigFileContent(result, this.host, getDirectoryPath(normalizedFileName), /*existingOptions*/ {}, normalizedFileName);
return {
options: configFile.options,
typeAcquisition: configFile.typeAcquisition,
files: configFile.fileNames,
raw: configFile.raw,
errors: realizeDiagnostics([...result.parseDiagnostics, ...configFile.errors], "\r\n")
};
});
}
public getDefaultCompilationSettings(): string {
return this.forwardJSONCall(
"getDefaultCompilationSettings()",
() => getDefaultCompilerOptions()
);
}
public discoverTypings(discoverTypingsJson: string): string {
const getCanonicalFileName = createGetCanonicalFileName(/*useCaseSensitivefileNames:*/ false);
return this.forwardJSONCall("discoverTypings()", () => {
const info = JSON.parse(discoverTypingsJson) as DiscoverTypingsInfo;
if (this.safeList === undefined) {
this.safeList = JsTyping.loadSafeList(this.host, toPath(info.safeListPath, info.safeListPath, getCanonicalFileName));
}
return JsTyping.discoverTypings(
this.host,
msg => this.logger.log(msg),
info.fileNames,
toPath(info.projectRootPath, info.projectRootPath, getCanonicalFileName),
this.safeList,
info.packageNameToTypingLocation,
info.typeAcquisition,
info.unresolvedImports,
info.typesRegistry);
});
}
}
export class TypeScriptServicesFactory implements ShimFactory {
private _shims: Shim[] = [];
private documentRegistry: DocumentRegistry | undefined;
/*
* Returns script API version.
*/
public getServicesVersion(): string {
return servicesVersion;
}
public createLanguageServiceShim(host: LanguageServiceShimHost): LanguageServiceShim {
try {
if (this.documentRegistry === undefined) {
this.documentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames(), host.getCurrentDirectory());
}
const hostAdapter = new LanguageServiceShimHostAdapter(host);
const languageService = createLanguageService(hostAdapter, this.documentRegistry, /*syntaxOnly*/ false);
return new LanguageServiceShimObject(this, host, languageService);
}
catch (err) {
logInternalError(host, err);
throw err;
}
}
public createClassifierShim(logger: Logger): ClassifierShim {
try {
return new ClassifierShimObject(this, logger);
}
catch (err) {
logInternalError(logger, err);
throw err;
}
}
public createCoreServicesShim(host: CoreServicesShimHost): CoreServicesShim {
try {
const adapter = new CoreServicesShimHostAdapter(host);
return new CoreServicesShimObject(this, host as Logger, adapter);
}
catch (err) {
logInternalError(host as Logger, err);
throw err;
}
}
public close(): void {
// Forget all the registered shims
clear(this._shims);
this.documentRegistry = undefined;
}
public registerShim(shim: Shim): void {
this._shims.push(shim);
}
public unregisterShim(shim: Shim): void {
for (let i = 0; i < this._shims.length; i++) {
if (this._shims[i] === shim) {
delete this._shims[i];
return;
}
}
throw new Error("Invalid operation");
}
}
}
/* eslint-enable no-in-operator */