/// /// namespace ts.server { export enum LogLevel { terse, normal, requestTime, verbose } export const emptyArray: SortedReadonlyArray = createSortedArray(); export interface Logger { close(): void; hasLevel(level: LogLevel): boolean; loggingEnabled(): boolean; perftrc(s: string): void; info(s: string): void; startGroup(): void; endGroup(): void; msg(s: string, type?: Msg.Types): void; getLogFileName(): string; } export namespace Msg { export type Err = "Err"; export const Err: Err = "Err"; export type Info = "Info"; export const Info: Info = "Info"; export type Perf = "Perf"; export const Perf: Perf = "Perf"; export type Types = Err | Info | Perf; } function getProjectRootPath(project: Project): Path { switch (project.projectKind) { case ProjectKind.Configured: return getDirectoryPath(project.getProjectName()); case ProjectKind.Inferred: // TODO: fixme return ""; case ProjectKind.External: const projectName = normalizeSlashes(project.getProjectName()); return project.projectService.host.fileExists(projectName) ? getDirectoryPath(projectName) : projectName; } } export function createInstallTypingsRequest(project: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray, cachePath?: string): DiscoverTypings { return { projectName: project.getProjectName(), fileNames: project.getFileNames(/*excludeFilesFromExternalLibraries*/ true, /*excludeConfigFiles*/ true), compilerOptions: project.getCompilerOptions(), typeAcquisition, unresolvedImports, projectRootPath: getProjectRootPath(project), cachePath, kind: "discover" }; } export namespace Errors { export function ThrowNoProject(): never { throw new Error("No Project."); } export function ThrowProjectLanguageServiceDisabled(): never { throw new Error("The project's language service is disabled."); } export function ThrowProjectDoesNotContainDocument(fileName: string, project: Project): never { throw new Error(`Project '${project.getProjectName()}' does not contain document '${fileName}'`); } } export function getDefaultFormatCodeSettings(host: ServerHost): FormatCodeSettings { return { indentSize: 4, tabSize: 4, newLineCharacter: host.newLine || "\n", convertTabsToSpaces: true, indentStyle: IndentStyle.Smart, insertSpaceAfterConstructor: false, insertSpaceAfterCommaDelimiter: true, insertSpaceAfterSemicolonInForStatements: true, insertSpaceBeforeAndAfterBinaryOperators: true, insertSpaceAfterKeywordsInControlFlowStatements: true, insertSpaceAfterFunctionKeywordForAnonymousFunctions: false, insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false, insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false, insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: true, insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false, insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: false, insertSpaceBeforeFunctionParenthesis: false, placeOpenBraceOnNewLineForFunctions: false, placeOpenBraceOnNewLineForControlBlocks: false, }; } export function mergeMapLikes(target: MapLike, source: MapLike ): void { for (const key in source) { if (hasProperty(source, key)) { target[key] = source[key]; } } } export type NormalizedPath = string & { __normalizedPathTag: any }; export function toNormalizedPath(fileName: string): NormalizedPath { return normalizePath(fileName); } export function normalizedPathToPath(normalizedPath: NormalizedPath, currentDirectory: string, getCanonicalFileName: (f: string) => string): Path { const f = isRootedDiskPath(normalizedPath) ? normalizedPath : getNormalizedAbsolutePath(normalizedPath, currentDirectory); return getCanonicalFileName(f); } export function asNormalizedPath(fileName: string): NormalizedPath { return fileName; } export interface NormalizedPathMap { get(path: NormalizedPath): T; set(path: NormalizedPath, value: T): void; contains(path: NormalizedPath): boolean; remove(path: NormalizedPath): void; } export function createNormalizedPathMap(): NormalizedPathMap { /* tslint:disable:no-null-keyword */ const map = createMap(); /* tslint:enable:no-null-keyword */ return { get(path) { return map.get(path); }, set(path, value) { map.set(path, value); }, contains(path) { return map.has(path); }, remove(path) { map.delete(path); } }; } export interface ProjectOptions { configHasExtendsProperty: boolean; /** * true if config file explicitly listed files */ configHasFilesProperty: boolean; configHasIncludeProperty: boolean; configHasExcludeProperty: boolean; /** * these fields can be present in the project file */ files?: string[]; wildcardDirectories?: Map; compilerOptions?: CompilerOptions; typeAcquisition?: TypeAcquisition; compileOnSave?: boolean; } export function isInferredProjectName(name: string) { // POSIX defines /dev/null as a device - there should be no file with this prefix return /dev\/null\/inferredProject\d+\*/.test(name); } export function makeInferredProjectName(counter: number) { return `/dev/null/inferredProject${counter}*`; } export function createSortedArray(): SortedArray { return [] as SortedArray; } export class ThrottledOperations { private pendingTimeouts: Map = createMap(); constructor(private readonly host: ServerHost) { } public schedule(operationId: string, delay: number, cb: () => void) { const pendingTimeout = this.pendingTimeouts.get(operationId); if (pendingTimeout) { // another operation was already scheduled for this id - cancel it this.host.clearTimeout(pendingTimeout); } // schedule new operation, pass arguments this.pendingTimeouts.set(operationId, this.host.setTimeout(ThrottledOperations.run, delay, this, operationId, cb)); } private static run(self: ThrottledOperations, operationId: string, cb: () => void) { self.pendingTimeouts.delete(operationId); cb(); } } export class GcTimer { private timerId: any; constructor(private readonly host: ServerHost, private readonly delay: number, private readonly logger: Logger) { } public scheduleCollect() { if (!this.host.gc || this.timerId !== undefined) { // no global.gc or collection was already scheduled - skip this request return; } this.timerId = this.host.setTimeout(GcTimer.run, this.delay, this); } private static run(self: GcTimer) { self.timerId = undefined; const log = self.logger.hasLevel(LogLevel.requestTime); const before = log && self.host.getMemoryUsage(); self.host.gc(); if (log) { const after = self.host.getMemoryUsage(); self.logger.perftrc(`GC::before ${before}, after ${after}`); } } } } /* @internal */ namespace ts.server { export function insertSorted(array: SortedArray, insert: T, compare: Comparer): void { if (array.length === 0) { array.push(insert); return; } const insertIndex = binarySearch(array, insert, compare); if (insertIndex < 0) { array.splice(~insertIndex, 0, insert); } } export function removeSorted(array: SortedArray, remove: T, compare: Comparer): void { if (!array || array.length === 0) { return; } if (array[0] === remove) { array.splice(0, 1); return; } const removeIndex = binarySearch(array, remove, compare); if (removeIndex >= 0) { array.splice(removeIndex, 1); } } export function toSortedArray(arr: string[]): SortedArray; export function toSortedArray(arr: T[], comparer: Comparer): SortedArray; export function toSortedArray(arr: T[], comparer?: Comparer): SortedArray { arr.sort(comparer); return arr as SortedArray; } export function enumerateInsertsAndDeletes(newItems: SortedReadonlyArray, oldItems: SortedReadonlyArray, inserted: (newItem: T) => void, deleted: (oldItem: T) => void, compare?: Comparer) { compare = compare || compareValues; let newIndex = 0; let oldIndex = 0; const newLen = newItems.length; const oldLen = oldItems.length; while (newIndex < newLen && oldIndex < oldLen) { const newItem = newItems[newIndex]; const oldItem = oldItems[oldIndex]; const compareResult = compare(newItem, oldItem); if (compareResult === Comparison.LessThan) { inserted(newItem); newIndex++; } else if (compareResult === Comparison.GreaterThan) { deleted(oldItem); oldIndex++; } else { newIndex++; oldIndex++; } } while (newIndex < newLen) { inserted(newItems[newIndex++]); } while (oldIndex < oldLen) { deleted(oldItems[oldIndex++]); } } export function cleanExistingMap( existingMap: Map, onDeleteExistingValue: (key: string, existingValue: T) => void) { if (existingMap) { // Remove all existingMap.forEach((existingValue, key) => { existingMap.delete(key); onDeleteExistingValue(key, existingValue); }); } } export function mutateExistingMapWithNewSet( existingMap: Map, newMap: Map, createNewValue: (key: string) => T, onDeleteExistingValue: (key: string, existingValue: T) => void ): Map { return mutateExistingMap( existingMap, newMap, // Same value if the value is set in the map /*isSameValue*/(_existingValue, _valueInNewMap) => true, /*createNewValue*/(key, _valueInNewMap) => createNewValue(key), onDeleteExistingValue, // Should never be called since we say yes to same values all the time /*OnDeleteExistingMismatchValue*/(_key, _existingValue) => notImplemented() ); } export function mutateExistingMap( existingMap: Map, newMap: Map, isSameValue: (existingValue: T, valueInNewMap: U) => boolean, createNewValue: (key: string, valueInNewMap: U) => T, onDeleteExistingValue: (key: string, existingValue: T) => void, OnDeleteExistingMismatchValue: (key: string, existingValue: T) => void ): Map { // If there are new values update them if (newMap) { if (existingMap) { // Needs update existingMap.forEach((existingValue, key) => { const valueInNewMap = newMap.get(key); // Existing value - remove it if (valueInNewMap === undefined) { existingMap.delete(key); onDeleteExistingValue(key, existingValue); } // different value - remove it else if (!isSameValue(existingValue, valueInNewMap)) { existingMap.delete(key); OnDeleteExistingMismatchValue(key, existingValue); } }); } else { // Create new existingMap = createMap(); } // Add new values that are not already present newMap.forEach((valueInNewMap, key) => { if (!existingMap.has(key)) { // New values existingMap.set(key, createNewValue(key, valueInNewMap)); } }); return existingMap; } cleanExistingMap(existingMap, onDeleteExistingValue); return undefined; } }