namespace ts { // WARNING: The script `configureNightly.ts` uses a regexp to parse out these values. // If changing the text in this section, be sure to test `configureNightly` too. export const versionMajorMinor = "3.0"; /** The version of the TypeScript compiler release */ export const version = `${versionMajorMinor}.0-dev`; } namespace ts { /** * Type of objects whose values are all of the same type. * The `in` and `for-in` operators can *not* be safely used, * since `Object.prototype` may be modified by outside code. */ export interface MapLike { [index: string]: T; } export interface SortedArray extends Array { " __sortedArrayBrand": any; } /** ES6 Map interface, only read methods included. */ export interface ReadonlyMap { get(key: string): T | undefined; has(key: string): boolean; forEach(action: (value: T, key: string) => void): void; readonly size: number; keys(): Iterator; values(): Iterator; entries(): Iterator<[string, T]>; } /** ES6 Map interface. */ export interface Map extends ReadonlyMap { set(key: string, value: T): this; delete(key: string): boolean; clear(): void; } /** ES6 Iterator type. */ export interface Iterator { next(): { value: T, done: false } | { value: never, done: true }; } /** Array that is only intended to be pushed to, never read. */ export interface Push { push(...values: T[]): void; } /* @internal */ export type EqualityComparer = (a: T, b: T) => boolean; /* @internal */ export type Comparer = (a: T, b: T) => Comparison; /* @internal */ export const enum Comparison { LessThan = -1, EqualTo = 0, GreaterThan = 1 } } /* @internal */ namespace ts { /** Create a MapLike with good performance. */ function createDictionaryObject(): MapLike { const map = Object.create(/*prototype*/ null); // tslint:disable-line:no-null-keyword // Using 'delete' on an object causes V8 to put the object in dictionary mode. // This disables creation of hidden classes, which are expensive when an object is // constantly changing shape. map.__ = undefined; delete map.__; return map; } /** Create a new map. If a template object is provided, the map will copy entries from it. */ export function createMap(): Map { return new MapCtr(); } export function createMapFromEntries(entries: [string, T][]): Map { const map = createMap(); for (const [key, value] of entries) { map.set(key, value); } return map; } export function createMapFromTemplate(template: MapLike): Map { const map: Map = new MapCtr(); // Copies keys/values from template. Note that for..in will not throw if // template is undefined, and instead will just exit the loop. for (const key in template) { if (hasOwnProperty.call(template, key)) { map.set(key, template[key]); } } return map; } // The global Map object. This may not be available, so we must test for it. declare const Map: { new (): Map } | undefined; // Internet Explorer's Map doesn't support iteration, so don't use it. // tslint:disable-next-line no-in-operator variable-name export const MapCtr = typeof Map !== "undefined" && "entries" in Map.prototype ? Map : shimMap(); // Keep the class inside a function so it doesn't get compiled if it's not used. function shimMap(): { new (): Map } { class MapIterator { private data: MapLike; private keys: ReadonlyArray; private index = 0; private selector: (data: MapLike, key: string) => U; constructor(data: MapLike, selector: (data: MapLike, key: string) => U) { this.data = data; this.selector = selector; this.keys = Object.keys(data); } public next(): { value: U, done: false } | { value: never, done: true } { const index = this.index; if (index < this.keys.length) { this.index++; return { value: this.selector(this.data, this.keys[index]), done: false }; } return { value: undefined as never, done: true }; } } return class implements Map { private data = createDictionaryObject(); public size = 0; get(key: string): T | undefined { return this.data[key]; } set(key: string, value: T): this { if (!this.has(key)) { this.size++; } this.data[key] = value; return this; } has(key: string): boolean { // tslint:disable-next-line:no-in-operator return key in this.data; } delete(key: string): boolean { if (this.has(key)) { this.size--; delete this.data[key]; return true; } return false; } clear(): void { this.data = createDictionaryObject(); this.size = 0; } keys(): Iterator { return new MapIterator(this.data, (_data, key) => key); } values(): Iterator { return new MapIterator(this.data, (data, key) => data[key]); } entries(): Iterator<[string, T]> { return new MapIterator(this.data, (data, key) => [key, data[key]] as [string, T]); } forEach(action: (value: T, key: string) => void): void { for (const key in this.data) { action(this.data[key], key); } } }; } export function length(array: ReadonlyArray | undefined): number { return array ? array.length : 0; } /** * Iterates through 'array' by index and performs the callback on each element of array until the callback * returns a truthy value, then returns that value. * If no such value is found, the callback is applied to each element of array and undefined is returned. */ export function forEach(array: ReadonlyArray | undefined, callback: (element: T, index: number) => U | undefined): U | undefined { if (array) { for (let i = 0; i < array.length; i++) { const result = callback(array[i], i); if (result) { return result; } } } return undefined; } /** Like `forEach`, but suitable for use with numbers and strings (which may be falsy). */ export function firstDefined(array: ReadonlyArray | undefined, callback: (element: T, index: number) => U | undefined): U | undefined { if (array === undefined) { return undefined; } for (let i = 0; i < array.length; i++) { const result = callback(array[i], i); if (result !== undefined) { return result; } } return undefined; } export function firstDefinedIterator(iter: Iterator, callback: (element: T) => U | undefined): U | undefined { while (true) { const { value, done } = iter.next(); if (done) { return undefined; } const result = callback(value); if (result !== undefined) { return result; } } } export function zipWith(arrayA: ReadonlyArray, arrayB: ReadonlyArray, callback: (a: T, b: U, index: number) => V): V[] { const result: V[] = []; Debug.assertEqual(arrayA.length, arrayB.length); for (let i = 0; i < arrayA.length; i++) { result.push(callback(arrayA[i], arrayB[i], i)); } return result; } export function zipToIterator(arrayA: ReadonlyArray, arrayB: ReadonlyArray): Iterator<[T, U]> { Debug.assertEqual(arrayA.length, arrayB.length); let i = 0; return { next() { if (i === arrayA.length) { return { value: undefined as never, done: true }; } i++; return { value: [arrayA[i - 1], arrayB[i - 1]], done: false }; } }; } export function zipToMap(keys: ReadonlyArray, values: ReadonlyArray): Map { Debug.assert(keys.length === values.length); const map = createMap(); for (let i = 0; i < keys.length; ++i) { map.set(keys[i], values[i]); } return map; } /** * Iterates through `array` by index and performs the callback on each element of array until the callback * returns a falsey value, then returns false. * If no such value is found, the callback is applied to each element of array and `true` is returned. */ export function every(array: ReadonlyArray, callback: (element: T, index: number) => boolean): boolean { if (array) { for (let i = 0; i < array.length; i++) { if (!callback(array[i], i)) { return false; } } } return true; } /** Works like Array.prototype.find, returning `undefined` if no element satisfying the predicate is found. */ export function find(array: ReadonlyArray, predicate: (element: T, index: number) => element is U): U | undefined; export function find(array: ReadonlyArray, predicate: (element: T, index: number) => boolean): T | undefined; export function find(array: ReadonlyArray, predicate: (element: T, index: number) => boolean): T | undefined { for (let i = 0; i < array.length; i++) { const value = array[i]; if (predicate(value, i)) { return value; } } return undefined; } export function findLast(array: ReadonlyArray, predicate: (element: T, index: number) => element is U): U | undefined; export function findLast(array: ReadonlyArray, predicate: (element: T, index: number) => boolean): T | undefined; export function findLast(array: ReadonlyArray, predicate: (element: T, index: number) => boolean): T | undefined { for (let i = array.length - 1; i >= 0; i--) { const value = array[i]; if (predicate(value, i)) { return value; } } return undefined; } /** Works like Array.prototype.findIndex, returning `-1` if no element satisfying the predicate is found. */ export function findIndex(array: ReadonlyArray, predicate: (element: T, index: number) => boolean, startIndex?: number): number { for (let i = startIndex || 0; i < array.length; i++) { if (predicate(array[i], i)) { return i; } } return -1; } export function findLastIndex(array: ReadonlyArray, predicate: (element: T, index: number) => boolean, startIndex?: number): number { for (let i = startIndex === undefined ? array.length - 1 : startIndex; i >= 0; i--) { if (predicate(array[i], i)) { return i; } } return -1; } /** * Returns the first truthy result of `callback`, or else fails. * This is like `forEach`, but never returns undefined. */ export function findMap(array: ReadonlyArray, callback: (element: T, index: number) => U | undefined): U { for (let i = 0; i < array.length; i++) { const result = callback(array[i], i); if (result) { return result; } } return Debug.fail(); } export function contains(array: ReadonlyArray | undefined, value: T, equalityComparer: EqualityComparer = equateValues): boolean { if (array) { for (const v of array) { if (equalityComparer(v, value)) { return true; } } } return false; } export function arraysEqual(a: ReadonlyArray, b: ReadonlyArray, equalityComparer: EqualityComparer = equateValues): boolean { return a.length === b.length && a.every((x, i) => equalityComparer(x, b[i])); } export function indexOfAnyCharCode(text: string, charCodes: ReadonlyArray, start?: number): number { for (let i = start || 0; i < text.length; i++) { if (contains(charCodes, text.charCodeAt(i))) { return i; } } return -1; } export function countWhere(array: ReadonlyArray, predicate: (x: T, i: number) => boolean): number { let count = 0; if (array) { for (let i = 0; i < array.length; i++) { const v = array[i]; if (predicate(v, i)) { count++; } } } return count; } /** * Filters an array by a predicate function. Returns the same array instance if the predicate is * true for all elements, otherwise returns a new array instance containing the filtered subset. */ export function filter(array: T[], f: (x: T) => x is U): U[]; export function filter(array: T[], f: (x: T) => boolean): T[]; export function filter(array: ReadonlyArray, f: (x: T) => x is U): ReadonlyArray; export function filter(array: ReadonlyArray, f: (x: T) => boolean): ReadonlyArray; export function filter(array: T[] | undefined, f: (x: T) => x is U): U[] | undefined; export function filter(array: T[] | undefined, f: (x: T) => boolean): T[] | undefined; export function filter(array: ReadonlyArray | undefined, f: (x: T) => x is U): ReadonlyArray | undefined; export function filter(array: ReadonlyArray | undefined, f: (x: T) => boolean): ReadonlyArray | undefined; export function filter(array: ReadonlyArray | undefined, f: (x: T) => boolean): ReadonlyArray | undefined { if (array) { const len = array.length; let i = 0; while (i < len && f(array[i])) i++; if (i < len) { const result = array.slice(0, i); i++; while (i < len) { const item = array[i]; if (f(item)) { result.push(item); } i++; } return result; } } return array; } export function filterMutate(array: T[], f: (x: T, i: number, array: T[]) => boolean): void { let outIndex = 0; for (let i = 0; i < array.length; i++) { if (f(array[i], i, array)) { array[outIndex] = array[i]; outIndex++; } } array.length = outIndex; } export function clear(array: {}[]): void { array.length = 0; } export function map(array: ReadonlyArray, f: (x: T, i: number) => U): U[]; export function map(array: ReadonlyArray | undefined, f: (x: T, i: number) => U): U[] | undefined; export function map(array: ReadonlyArray | undefined, f: (x: T, i: number) => U): U[] | undefined { let result: U[] | undefined; if (array) { result = []; for (let i = 0; i < array.length; i++) { result.push(f(array[i], i)); } } return result; } export function mapIterator(iter: Iterator, mapFn: (x: T) => U): Iterator { return { next() { const iterRes = iter.next(); return iterRes.done ? iterRes : { value: mapFn(iterRes.value), done: false }; } }; } // Maps from T to T and avoids allocation if all elements map to themselves export function sameMap(array: T[], f: (x: T, i: number) => T): T[]; export function sameMap(array: ReadonlyArray, f: (x: T, i: number) => T): ReadonlyArray; export function sameMap(array: T[] | undefined, f: (x: T, i: number) => T): T[] | undefined; export function sameMap(array: ReadonlyArray | undefined, f: (x: T, i: number) => T): ReadonlyArray | undefined; export function sameMap(array: ReadonlyArray | undefined, f: (x: T, i: number) => T): ReadonlyArray | undefined { if (array) { for (let i = 0; i < array.length; i++) { const item = array[i]; const mapped = f(item, i); if (item !== mapped) { const result = array.slice(0, i); result.push(mapped); for (i++; i < array.length; i++) { result.push(f(array[i], i)); } return result; } } } return array; } /** * Flattens an array containing a mix of array or non-array elements. * * @param array The array to flatten. */ export function flatten(array: ReadonlyArray | undefined>): T[]; export function flatten(array: ReadonlyArray | undefined> | undefined): T[] | undefined; export function flatten(array: ReadonlyArray | undefined> | undefined): T[] | undefined { let result: T[] | undefined; if (array) { result = []; for (const v of array) { if (v) { if (isArray(v)) { addRange(result, v); } else { result.push(v); } } } } return result; } /** * Maps an array. If the mapped value is an array, it is spread into the result. * * @param array The array to map. * @param mapfn The callback used to map the result into one or more values. */ export function flatMap(array: ReadonlyArray, mapfn: (x: T, i: number) => U | ReadonlyArray | undefined): U[]; export function flatMap(array: ReadonlyArray | undefined, mapfn: (x: T, i: number) => U | ReadonlyArray | undefined): U[] | undefined; export function flatMap(array: ReadonlyArray | undefined, mapfn: (x: T, i: number) => U | ReadonlyArray | undefined): U[] | undefined { let result: U[] | undefined; if (array) { result = []; for (let i = 0; i < array.length; i++) { const v = mapfn(array[i], i); if (v) { if (isArray(v)) { addRange(result, v); } else { result.push(v); } } } } return result; } export function flatMapIterator(iter: Iterator, mapfn: (x: T) => ReadonlyArray | Iterator | undefined): Iterator { const first = iter.next(); if (first.done) { return emptyIterator; } let currentIter = getIterator(first.value); return { next() { while (true) { const currentRes = currentIter.next(); if (!currentRes.done) { return currentRes; } const iterRes = iter.next(); if (iterRes.done) { return iterRes; } currentIter = getIterator(iterRes.value); } }, }; function getIterator(x: T): Iterator { const res = mapfn(x); return res === undefined ? emptyIterator : isArray(res) ? arrayIterator(res) : res; } } /** * Maps an array. If the mapped value is an array, it is spread into the result. * Avoids allocation if all elements map to themselves. * * @param array The array to map. * @param mapfn The callback used to map the result into one or more values. */ export function sameFlatMap(array: T[], mapfn: (x: T, i: number) => T | ReadonlyArray): T[]; export function sameFlatMap(array: ReadonlyArray, mapfn: (x: T, i: number) => T | ReadonlyArray): ReadonlyArray; export function sameFlatMap(array: T[], mapfn: (x: T, i: number) => T | T[]): T[] { let result: T[] | undefined; if (array) { for (let i = 0; i < array.length; i++) { const item = array[i]; const mapped = mapfn(item, i); if (result || item !== mapped || isArray(mapped)) { if (!result) { result = array.slice(0, i); } if (isArray(mapped)) { addRange(result, mapped); } else { result.push(mapped); } } } } return result || array; } export function mapAllOrFail(array: ReadonlyArray, mapFn: (x: T, i: number) => U | undefined): U[] | undefined { const result: U[] = []; for (let i = 0; i < array.length; i++) { const mapped = mapFn(array[i], i); if (mapped === undefined) { return undefined; } result.push(mapped); } return result; } export function mapDefined(array: ReadonlyArray | undefined, mapFn: (x: T, i: number) => U | undefined): U[] { const result: U[] = []; if (array) { for (let i = 0; i < array.length; i++) { const mapped = mapFn(array[i], i); if (mapped !== undefined) { result.push(mapped); } } } return result; } export function mapDefinedIterator(iter: Iterator, mapFn: (x: T) => U | undefined): Iterator { return { next() { while (true) { const res = iter.next(); if (res.done) { return res; } const value = mapFn(res.value); if (value !== undefined) { return { value, done: false }; } } } }; } export const emptyIterator: Iterator = { next: () => ({ value: undefined as never, done: true }) }; export function singleIterator(value: T): Iterator { let done = false; return { next() { const wasDone = done; done = true; return wasDone ? { value: undefined as never, done: true } : { value, done: false }; } }; } /** * Maps contiguous spans of values with the same key. * * @param array The array to map. * @param keyfn A callback used to select the key for an element. * @param mapfn A callback used to map a contiguous chunk of values to a single value. */ export function spanMap(array: ReadonlyArray, keyfn: (x: T, i: number) => K, mapfn: (chunk: T[], key: K, start: number, end: number) => U): U[]; export function spanMap(array: ReadonlyArray | undefined, keyfn: (x: T, i: number) => K, mapfn: (chunk: T[], key: K, start: number, end: number) => U): U[] | undefined; export function spanMap(array: ReadonlyArray | undefined, keyfn: (x: T, i: number) => K, mapfn: (chunk: T[], key: K, start: number, end: number) => U): U[] | undefined { let result: U[] | undefined; if (array) { result = []; const len = array.length; let previousKey: K | undefined; let key: K | undefined; let start = 0; let pos = 0; while (start < len) { while (pos < len) { const value = array[pos]; key = keyfn(value, pos); if (pos === 0) { previousKey = key; } else if (key !== previousKey) { break; } pos++; } if (start < pos) { const v = mapfn(array.slice(start, pos), previousKey!, start, pos); if (v) { result.push(v); } start = pos; } previousKey = key; pos++; } } return result; } export function mapEntries(map: ReadonlyMap, f: (key: string, value: T) => [string, U]): Map; export function mapEntries(map: ReadonlyMap | undefined, f: (key: string, value: T) => [string, U]): Map | undefined; export function mapEntries(map: ReadonlyMap | undefined, f: (key: string, value: T) => [string, U]): Map | undefined { if (!map) { return undefined; } const result = createMap(); map.forEach((value, key) => { const [newKey, newValue] = f(key, value); result.set(newKey, newValue); }); return result; } export function some(array: ReadonlyArray | undefined): array is ReadonlyArray; export function some(array: ReadonlyArray | undefined, predicate: (value: T) => boolean): boolean; export function some(array: ReadonlyArray | undefined, predicate?: (value: T) => boolean): boolean { if (array) { if (predicate) { for (const v of array) { if (predicate(v)) { return true; } } } else { return array.length > 0; } } return false; } /** Calls the callback with (start, afterEnd) index pairs for each range where 'pred' is true. */ export function getRangesWhere(arr: ReadonlyArray, pred: (t: T) => boolean, cb: (start: number, afterEnd: number) => void): void { let start: number | undefined; for (let i = 0; i < arr.length; i++) { if (pred(arr[i])) { start = start === undefined ? i : start; } else { if (start !== undefined) { cb(start, i); start = undefined; } } } if (start !== undefined) cb(start, arr.length); } export function concatenate(array1: T[], array2: T[]): T[]; export function concatenate(array1: ReadonlyArray, array2: ReadonlyArray): ReadonlyArray; export function concatenate(array1: T[] | undefined, array2: T[] | undefined): T[]; export function concatenate(array1: ReadonlyArray | undefined, array2: ReadonlyArray | undefined): ReadonlyArray; export function concatenate(array1: T[], array2: T[]): T[] { if (!some(array2)) return array1; if (!some(array1)) return array2; return [...array1, ...array2]; } function deduplicateRelational(array: ReadonlyArray, equalityComparer: EqualityComparer, comparer: Comparer) { // Perform a stable sort of the array. This ensures the first entry in a list of // duplicates remains the first entry in the result. const indices = array.map((_, i) => i); stableSortIndices(array, indices, comparer); let last = array[indices[0]]; const deduplicated: number[] = [indices[0]]; for (let i = 1; i < indices.length; i++) { const index = indices[i]; const item = array[index]; if (!equalityComparer(last, item)) { deduplicated.push(index); last = item; } } // restore original order deduplicated.sort(); return deduplicated.map(i => array[i]); } function deduplicateEquality(array: ReadonlyArray, equalityComparer: EqualityComparer) { const result: T[] = []; for (const item of array) { pushIfUnique(result, item, equalityComparer); } return result; } /** * Deduplicates an unsorted array. * @param equalityComparer An optional `EqualityComparer` used to determine if two values are duplicates. * @param comparer An optional `Comparer` used to sort entries before comparison, though the * result will remain in the original order in `array`. */ export function deduplicate(array: ReadonlyArray, equalityComparer?: EqualityComparer, comparer?: Comparer): T[]; export function deduplicate(array: ReadonlyArray | undefined, equalityComparer?: EqualityComparer, comparer?: Comparer): T[] | undefined; export function deduplicate(array: ReadonlyArray | undefined, equalityComparer: EqualityComparer, comparer?: Comparer): T[] | undefined { return !array ? undefined : array.length === 0 ? [] : array.length === 1 ? array.slice() : comparer ? deduplicateRelational(array, equalityComparer, comparer) : deduplicateEquality(array, equalityComparer); } /** * Deduplicates an array that has already been sorted. */ function deduplicateSorted(array: ReadonlyArray, comparer: EqualityComparer | Comparer): T[]; function deduplicateSorted(array: ReadonlyArray | undefined, comparer: EqualityComparer | Comparer): T[] | undefined; function deduplicateSorted(array: ReadonlyArray | undefined, comparer: EqualityComparer | Comparer): T[] | undefined { if (!array) return undefined; if (array.length === 0) return []; let last = array[0]; const deduplicated: T[] = [last]; for (let i = 1; i < array.length; i++) { const next = array[i]; switch (comparer(next, last)) { // equality comparison case true: // relational comparison case Comparison.EqualTo: continue; case Comparison.LessThan: // If `array` is sorted, `next` should **never** be less than `last`. return Debug.fail("Array is unsorted."); } deduplicated.push(last = next); } return deduplicated; } export function insertSorted(array: SortedArray, insert: T, compare: Comparer): void { if (array.length === 0) { array.push(insert); return; } const insertIndex = binarySearch(array, insert, identity, compare); if (insertIndex < 0) { array.splice(~insertIndex, 0, insert); } } export function sortAndDeduplicate(array: ReadonlyArray, comparer: Comparer, equalityComparer?: EqualityComparer) { return deduplicateSorted(sort(array, comparer), equalityComparer || comparer); } export function arrayIsEqualTo(array1: ReadonlyArray | undefined, array2: ReadonlyArray | undefined, equalityComparer: (a: T, b: T) => boolean = equateValues): boolean { if (!array1 || !array2) { return array1 === array2; } if (array1.length !== array2.length) { return false; } for (let i = 0; i < array1.length; i++) { if (!equalityComparer(array1[i], array2[i])) { return false; } } return true; } /** * Compacts an array, removing any falsey elements. */ export function compact(array: T[]): T[]; export function compact(array: ReadonlyArray): ReadonlyArray; export function compact(array: T[]): T[] { let result: T[] | undefined; if (array) { for (let i = 0; i < array.length; i++) { const v = array[i]; if (result || !v) { if (!result) { result = array.slice(0, i); } if (v) { result.push(v); } } } } return result || array; } /** * Gets the relative complement of `arrayA` with respect to `arrayB`, returning the elements that * are not present in `arrayA` but are present in `arrayB`. Assumes both arrays are sorted * based on the provided comparer. */ export function relativeComplement(arrayA: T[] | undefined, arrayB: T[] | undefined, comparer: Comparer): T[] | undefined { if (!arrayB || !arrayA || arrayB.length === 0 || arrayA.length === 0) return arrayB; const result: T[] = []; loopB: for (let offsetA = 0, offsetB = 0; offsetB < arrayB.length; offsetB++) { if (offsetB > 0) { // Ensure `arrayB` is properly sorted. Debug.assertGreaterThanOrEqual(comparer(arrayB[offsetB], arrayB[offsetB - 1]), Comparison.EqualTo); } loopA: for (const startA = offsetA; offsetA < arrayA.length; offsetA++) { if (offsetA > startA) { // Ensure `arrayA` is properly sorted. We only need to perform this check if // `offsetA` has changed since we entered the loop. Debug.assertGreaterThanOrEqual(comparer(arrayA[offsetA], arrayA[offsetA - 1]), Comparison.EqualTo); } switch (comparer(arrayB[offsetB], arrayA[offsetA])) { case Comparison.LessThan: // If B is less than A, B does not exist in arrayA. Add B to the result and // move to the next element in arrayB without changing the current position // in arrayA. result.push(arrayB[offsetB]); continue loopB; case Comparison.EqualTo: // If B is equal to A, B exists in arrayA. Move to the next element in // arrayB without adding B to the result or changing the current position // in arrayA. continue loopB; case Comparison.GreaterThan: // If B is greater than A, we need to keep looking for B in arrayA. Move to // the next element in arrayA and recheck. continue loopA; } } } return result; } export function sum, K extends string>(array: ReadonlyArray, prop: K): number { let result = 0; for (const v of array) { result += v[prop]; } return result; } /** * Appends a value to an array, returning the array. * * @param to The array to which `value` is to be appended. If `to` is `undefined`, a new array * is created if `value` was appended. * @param value The value to append to the array. If `value` is `undefined`, nothing is * appended. */ export function append(to: T[], value: T | undefined): T[]; export function append(to: T[] | undefined, value: T): T[]; export function append(to: T[] | undefined, value: T | undefined): T[] | undefined; export function append(to: Push, value: T | undefined): void; export function append(to: T[], value: T | undefined): T[] | undefined { if (value === undefined) return to; if (to === undefined) return [value]; to.push(value); return to; } /** * Gets the actual offset into an array for a relative offset. Negative offsets indicate a * position offset from the end of the array. */ function toOffset(array: ReadonlyArray, offset: number) { return offset < 0 ? array.length + offset : offset; } /** * Appends a range of value to an array, returning the array. * * @param to The array to which `value` is to be appended. If `to` is `undefined`, a new array * is created if `value` was appended. * @param from The values to append to the array. If `from` is `undefined`, nothing is * appended. If an element of `from` is `undefined`, that element is not appended. * @param start The offset in `from` at which to start copying values. * @param end The offset in `from` at which to stop copying values (non-inclusive). */ export function addRange(to: T[], from: ReadonlyArray | undefined, start?: number, end?: number): T[]; export function addRange(to: T[] | undefined, from: ReadonlyArray | undefined, start?: number, end?: number): T[] | undefined; export function addRange(to: T[] | undefined, from: ReadonlyArray | undefined, start?: number, end?: number): T[] | undefined { if (from === undefined || from.length === 0) return to; if (to === undefined) return from.slice(start, end); start = start === undefined ? 0 : toOffset(from, start); end = end === undefined ? from.length : toOffset(from, end); for (let i = start; i < end && i < from.length; i++) { if (from[i] !== undefined) { to.push(from[i]); } } return to; } /** * @return Whether the value was added. */ export function pushIfUnique(array: T[], toAdd: T, equalityComparer?: EqualityComparer): boolean { if (contains(array, toAdd, equalityComparer)) { return false; } else { array.push(toAdd); return true; } } /** * Unlike `pushIfUnique`, this can take `undefined` as an input, and returns a new array. */ export function appendIfUnique(array: T[] | undefined, toAdd: T, equalityComparer?: EqualityComparer): T[] { if (array) { pushIfUnique(array, toAdd, equalityComparer); return array; } else { return [toAdd]; } } function stableSortIndices(array: ReadonlyArray, indices: number[], comparer: Comparer) { // sort indices by value then position indices.sort((x, y) => comparer(array[x], array[y]) || compareValues(x, y)); } /** * Returns a new sorted array. */ export function sort(array: ReadonlyArray, comparer: Comparer): T[] { return array.slice().sort(comparer); } export function arrayIterator(array: ReadonlyArray): Iterator { let i = 0; return { next: () => { if (i === array.length) { return { value: undefined as never, done: true }; } else { i++; return { value: array[i - 1], done: false }; } }}; } /** * Stable sort of an array. Elements equal to each other maintain their relative position in the array. */ export function stableSort(array: ReadonlyArray, comparer: Comparer) { const indices = array.map((_, i) => i); stableSortIndices(array, indices, comparer); return indices.map(i => array[i]); } export function rangeEquals(array1: ReadonlyArray, array2: ReadonlyArray, pos: number, end: number) { while (pos < end) { if (array1[pos] !== array2[pos]) { return false; } pos++; } return true; } /** * Returns the element at a specific offset in an array if non-empty, `undefined` otherwise. * A negative offset indicates the element should be retrieved from the end of the array. */ export function elementAt(array: ReadonlyArray | undefined, offset: number): T | undefined { if (array) { offset = toOffset(array, offset); if (offset < array.length) { return array[offset]; } } return undefined; } /** * Returns the first element of an array if non-empty, `undefined` otherwise. */ export function firstOrUndefined(array: ReadonlyArray): T | undefined { return array.length === 0 ? undefined : array[0]; } export function first(array: ReadonlyArray): T { Debug.assert(array.length !== 0); return array[0]; } /** * Returns the last element of an array if non-empty, `undefined` otherwise. */ export function lastOrUndefined(array: ReadonlyArray): T | undefined { return array.length === 0 ? undefined : array[array.length - 1]; } export function last(array: ReadonlyArray): T { Debug.assert(array.length !== 0); return array[array.length - 1]; } /** * Returns the only element of an array if it contains only one element, `undefined` otherwise. */ export function singleOrUndefined(array: ReadonlyArray | undefined): T | undefined { return array && array.length === 1 ? array[0] : undefined; } /** * Returns the only element of an array if it contains only one element; otheriwse, returns the * array. */ export function singleOrMany(array: T[]): T | T[]; export function singleOrMany(array: ReadonlyArray): T | ReadonlyArray; export function singleOrMany(array: T[] | undefined): T | T[] | undefined; export function singleOrMany(array: ReadonlyArray | undefined): T | ReadonlyArray | undefined; export function singleOrMany(array: ReadonlyArray | undefined): T | ReadonlyArray | undefined { return array && array.length === 1 ? array[0] : array; } export function replaceElement(array: ReadonlyArray, index: number, value: T): T[] { const result = array.slice(0); result[index] = value; return result; } /** * Performs a binary search, finding the index at which `value` occurs in `array`. * If no such index is found, returns the 2's-complement of first index at which * `array[index]` exceeds `value`. * @param array A sorted array whose first element must be no larger than number * @param value The value to be searched for in the array. * @param keySelector A callback used to select the search key from `value` and each element of * `array`. * @param keyComparer A callback used to compare two keys in a sorted array. * @param offset An offset into `array` at which to start the search. */ export function binarySearch(array: ReadonlyArray, value: T, keySelector: (v: T) => U, keyComparer: Comparer, offset?: number): number { if (!array || array.length === 0) { return -1; } let low = offset || 0; let high = array.length - 1; const key = keySelector(value); while (low <= high) { const middle = low + ((high - low) >> 1); const midKey = keySelector(array[middle]); switch (keyComparer(midKey, key)) { case Comparison.LessThan: low = middle + 1; break; case Comparison.EqualTo: return middle; case Comparison.GreaterThan: high = middle - 1; break; } } return ~low; } export function reduceLeft(array: ReadonlyArray | undefined, f: (memo: U, value: T, i: number) => U, initial: U, start?: number, count?: number): U; export function reduceLeft(array: ReadonlyArray, f: (memo: T, value: T, i: number) => T): T | undefined; export function reduceLeft(array: T[], f: (memo: T, value: T, i: number) => T, initial?: T, start?: number, count?: number): T | undefined { if (array && array.length > 0) { const size = array.length; if (size > 0) { let pos = start === undefined || start < 0 ? 0 : start; const end = count === undefined || pos + count > size - 1 ? size - 1 : pos + count; let result: T; if (arguments.length <= 2) { result = array[pos]; pos++; } else { result = initial!; } while (pos <= end) { result = f(result, array[pos], pos); pos++; } return result; } } return initial; } const hasOwnProperty = Object.prototype.hasOwnProperty; /** * Indicates whether a map-like contains an own property with the specified key. * * @param map A map-like. * @param key A property key. */ export function hasProperty(map: MapLike, key: string): boolean { return hasOwnProperty.call(map, key); } /** * Gets the value of an owned property in a map-like. * * @param map A map-like. * @param key A property key. */ export function getProperty(map: MapLike, key: string): T | undefined { return hasOwnProperty.call(map, key) ? map[key] : undefined; } /** * Gets the owned, enumerable property keys of a map-like. */ export function getOwnKeys(map: MapLike): string[] { const keys: string[] = []; for (const key in map) { if (hasOwnProperty.call(map, key)) { keys.push(key); } } return keys; } export function getOwnValues(sparseArray: T[]): T[] { const values: T[] = []; for (const key in sparseArray) { if (hasOwnProperty.call(sparseArray, key)) { values.push(sparseArray[key]); } } return values; } /** Shims `Array.from`. */ export function arrayFrom(iterator: Iterator, map: (t: T) => U): U[]; export function arrayFrom(iterator: Iterator): T[]; export function arrayFrom(iterator: Iterator, map?: (t: any) => any): any[] { const result: any[] = []; for (let { value, done } = iterator.next(); !done; { value, done } = iterator.next()) { result.push(map ? map(value) : value); } return result; } export function assign(t: T, ...args: (T | undefined)[]) { for (const arg of args) { for (const p in arg!) { if (hasProperty(arg!, p)) { t![p] = arg![p]; // TODO: GH#23368 } } } return t; } /** * Performs a shallow equality comparison of the contents of two map-likes. * * @param left A map-like whose properties should be compared. * @param right A map-like whose properties should be compared. */ export function equalOwnProperties(left: MapLike | undefined, right: MapLike | undefined, equalityComparer: EqualityComparer = equateValues) { if (left === right) return true; if (!left || !right) return false; for (const key in left) { if (hasOwnProperty.call(left, key)) { if (!hasOwnProperty.call(right, key) === undefined) return false; if (!equalityComparer(left[key], right[key])) return false; } } for (const key in right) { if (hasOwnProperty.call(right, key)) { if (!hasOwnProperty.call(left, key)) return false; } } return true; } /** * Creates a map from the elements of an array. * * @param array the array of input elements. * @param makeKey a function that produces a key for a given element. * * This function makes no effort to avoid collisions; if any two elements produce * the same key with the given 'makeKey' function, then the element with the higher * index in the array will be the one associated with the produced key. */ export function arrayToMap(array: ReadonlyArray, makeKey: (value: T) => string | undefined): Map; export function arrayToMap(array: ReadonlyArray, makeKey: (value: T) => string | undefined, makeValue: (value: T) => U): Map; export function arrayToMap(array: ReadonlyArray, makeKey: (value: T) => string | undefined, makeValue: (value: T) => T | U = identity): Map { const result = createMap(); for (const value of array) { const key = makeKey(value); if (key !== undefined) result.set(key, makeValue(value)); } return result; } export function arrayToNumericMap(array: ReadonlyArray, makeKey: (value: T) => number): T[]; export function arrayToNumericMap(array: ReadonlyArray, makeKey: (value: T) => number, makeValue: (value: T) => U): U[]; export function arrayToNumericMap(array: ReadonlyArray, makeKey: (value: T) => number, makeValue: (value: T) => T | U = identity): (T | U)[] { const result: (T | U)[] = []; for (const value of array) { result[makeKey(value)] = makeValue(value); } return result; } export function arrayToMultiMap(values: ReadonlyArray, makeKey: (value: T) => string): MultiMap; export function arrayToMultiMap(values: ReadonlyArray, makeKey: (value: T) => string, makeValue: (value: T) => U): MultiMap; export function arrayToMultiMap(values: ReadonlyArray, makeKey: (value: T) => string, makeValue: (value: T) => T | U = identity): MultiMap { const result = createMultiMap(); for (const value of values) { result.add(makeKey(value), makeValue(value)); } return result; } export function group(values: ReadonlyArray, getGroupId: (value: T) => string): ReadonlyArray> { return arrayFrom(arrayToMultiMap(values, getGroupId).values()); } export function clone(object: T): T { const result: any = {}; for (const id in object) { if (hasOwnProperty.call(object, id)) { result[id] = (object)[id]; } } return result; } export function extend(first: T1, second: T2): T1 & T2 { const result: T1 & T2 = {}; for (const id in second) { if (hasOwnProperty.call(second, id)) { (result as any)[id] = (second as any)[id]; } } for (const id in first) { if (hasOwnProperty.call(first, id)) { (result as any)[id] = (first as any)[id]; } } return result; } export interface MultiMap extends Map { /** * Adds the value to an array of values associated with the key, and returns the array. * Creates the array if it does not already exist. */ add(key: string, value: T): T[]; /** * Removes a value from an array of values associated with the key. * Does not preserve the order of those values. * Does nothing if `key` is not in `map`, or `value` is not in `map[key]`. */ remove(key: string, value: T): void; } export function createMultiMap(): MultiMap { const map = createMap() as MultiMap; map.add = multiMapAdd; map.remove = multiMapRemove; return map; } function multiMapAdd(this: MultiMap, key: string, value: T) { let values = this.get(key); if (values) { values.push(value); } else { this.set(key, values = [value]); } return values; } function multiMapRemove(this: MultiMap, key: string, value: T) { const values = this.get(key); if (values) { unorderedRemoveItem(values, value); if (!values.length) { this.delete(key); } } } /** * Tests whether a value is an array. */ export function isArray(value: any): value is ReadonlyArray<{}> { return Array.isArray ? Array.isArray(value) : value instanceof Array; } export function toArray(value: T | T[]): T[]; export function toArray(value: T | ReadonlyArray): ReadonlyArray; export function toArray(value: T | T[]): T[] { return isArray(value) ? value : [value]; } /** * Tests whether a value is string */ export function isString(text: any): text is string { return typeof text === "string"; } export function tryCast(value: TIn | undefined, test: (value: TIn) => value is TOut): TOut | undefined; export function tryCast(value: T, test: (value: T) => boolean): T | undefined; export function tryCast(value: T, test: (value: T) => boolean): T | undefined { return value !== undefined && test(value) ? value : undefined; } export function cast(value: TIn | undefined, test: (value: TIn) => value is TOut): TOut { if (value !== undefined && test(value)) return value; return Debug.fail(`Invalid cast. The supplied value ${value} did not pass the test '${Debug.getFunctionName(test)}'.`); } /** Does nothing. */ export function noop(_?: {} | null | undefined): void { } // tslint:disable-line no-empty /** Do nothing and return false */ export function returnFalse(): false { return false; } /** Do nothing and return true */ export function returnTrue(): true { return true; } /** Returns its argument. */ export function identity(x: T) { return x; } /** Returns lower case string */ export function toLowerCase(x: string) { return x.toLowerCase(); } /** Throws an error because a function is not implemented. */ export function notImplemented(): never { throw new Error("Not implemented"); } export function memoize(callback: () => T): () => T { let value: T; return () => { if (callback) { value = callback(); callback = undefined!; } return value; }; } /** * High-order function, creates a function that executes a function composition. * For example, `chain(a, b)` is the equivalent of `x => ((a', b') => y => b'(a'(y)))(a(x), b(x))` * * @param args The functions to chain. */ export function chain(...args: ((t: T) => (u: U) => U)[]): (t: T) => (u: U) => U; export function chain(a: (t: T) => (u: U) => U, b: (t: T) => (u: U) => U, c: (t: T) => (u: U) => U, d: (t: T) => (u: U) => U, e: (t: T) => (u: U) => U): (t: T) => (u: U) => U { if (e) { const args: ((t: T) => (u: U) => U)[] = []; for (let i = 0; i < arguments.length; i++) { args[i] = arguments[i]; } return t => compose(...map(args, f => f(t))); } else if (d) { return t => compose(a(t), b(t), c(t), d(t)); } else if (c) { return t => compose(a(t), b(t), c(t)); } else if (b) { return t => compose(a(t), b(t)); } else if (a) { return t => compose(a(t)); } else { return _ => u => u; } } /** * High-order function, composes functions. Note that functions are composed inside-out; * for example, `compose(a, b)` is the equivalent of `x => b(a(x))`. * * @param args The functions to compose. */ export function compose(...args: ((t: T) => T)[]): (t: T) => T; export function compose(a: (t: T) => T, b: (t: T) => T, c: (t: T) => T, d: (t: T) => T, e: (t: T) => T): (t: T) => T { if (e) { const args: ((t: T) => T)[] = []; for (let i = 0; i < arguments.length; i++) { args[i] = arguments[i]; } return t => reduceLeft(args, (u, f) => f(u), t); } else if (d) { return t => d(c(b(a(t)))); } else if (c) { return t => c(b(a(t))); } else if (b) { return t => b(a(t)); } else if (a) { return t => a(t); } else { return t => t; } } export const enum AssertionLevel { None = 0, Normal = 1, Aggressive = 2, VeryAggressive = 3, } /** * Safer version of `Function` which should not be called. * Every function should be assignable to this, but this should not be assignable to every function. */ export type AnyFunction = (...args: never[]) => void; export namespace Debug { export let currentAssertionLevel = AssertionLevel.None; export let isDebugging = false; export function shouldAssert(level: AssertionLevel): boolean { return currentAssertionLevel >= level; } export function assert(expression: boolean, message?: string, verboseDebugInfo?: string | (() => string), stackCrawlMark?: AnyFunction): void { if (!expression) { if (verboseDebugInfo) { message += "\r\nVerbose Debug Information: " + (typeof verboseDebugInfo === "string" ? verboseDebugInfo : verboseDebugInfo()); } fail(message ? "False expression: " + message : "False expression.", stackCrawlMark || assert); } } export function assertEqual(a: T, b: T, msg?: string, msg2?: string): void { if (a !== b) { const message = msg ? msg2 ? `${msg} ${msg2}` : msg : ""; fail(`Expected ${a} === ${b}. ${message}`); } } export function assertLessThan(a: number, b: number, msg?: string): void { if (a >= b) { fail(`Expected ${a} < ${b}. ${msg || ""}`); } } export function assertLessThanOrEqual(a: number, b: number): void { if (a > b) { fail(`Expected ${a} <= ${b}`); } } export function assertGreaterThanOrEqual(a: number, b: number): void { if (a < b) { fail(`Expected ${a} >= ${b}`); } } export function fail(message?: string, stackCrawlMark?: AnyFunction): never { debugger; const e = new Error(message ? `Debug Failure. ${message}` : "Debug Failure."); if ((Error).captureStackTrace) { (Error).captureStackTrace(e, stackCrawlMark || fail); } throw e; } export function assertDefined(value: T | null | undefined, message?: string): T { if (value === undefined || value === null) return fail(message); return value; } export function assertEachDefined>(value: A, message?: string): A { for (const v of value) { assertDefined(v, message); } return value; } export function assertNever(member: never, message?: string, stackCrawlMark?: AnyFunction): never { return fail(message || `Illegal value: ${member}`, stackCrawlMark || assertNever); } export function getFunctionName(func: AnyFunction) { if (typeof func !== "function") { return ""; } else if (func.hasOwnProperty("name")) { return (func).name; } else { const text = Function.prototype.toString.call(func); const match = /^function\s+([\w\$]+)\s*\(/.exec(text); return match ? match[1] : ""; } } } export function equateValues(a: T, b: T) { return a === b; } /** * Compare the equality of two strings using a case-sensitive ordinal comparison. * * Case-sensitive comparisons compare both strings one code-point at a time using the integer * value of each code-point after applying `toUpperCase` to each string. We always map both * strings to their upper-case form as some unicode characters do not properly round-trip to * lowercase (such as `ẞ` (German sharp capital s)). */ export function equateStringsCaseInsensitive(a: string, b: string) { return a === b || a !== undefined && b !== undefined && a.toUpperCase() === b.toUpperCase(); } /** * Compare the equality of two strings using a case-sensitive ordinal comparison. * * Case-sensitive comparisons compare both strings one code-point at a time using the * integer value of each code-point. */ export function equateStringsCaseSensitive(a: string, b: string) { return equateValues(a, b); } function compareComparableValues(a: string | undefined, b: string | undefined): Comparison; function compareComparableValues(a: number | undefined, b: number | undefined): Comparison; function compareComparableValues(a: string | number | undefined, b: string | number | undefined) { return a === b ? Comparison.EqualTo : a === undefined ? Comparison.LessThan : b === undefined ? Comparison.GreaterThan : a < b ? Comparison.LessThan : Comparison.GreaterThan; } /** * Compare two numeric values for their order relative to each other. * To compare strings, use any of the `compareStrings` functions. */ export function compareValues(a: number | undefined, b: number | undefined): Comparison { return compareComparableValues(a, b); } export function min(a: T, b: T, compare: Comparer): T { return compare(a, b) === Comparison.LessThan ? a : b; } /** * Compare two strings using a case-insensitive ordinal comparison. * * Ordinal comparisons are based on the difference between the unicode code points of both * strings. Characters with multiple unicode representations are considered unequal. Ordinal * comparisons provide predictable ordering, but place "a" after "B". * * Case-insensitive comparisons compare both strings one code-point at a time using the integer * value of each code-point after applying `toUpperCase` to each string. We always map both * strings to their upper-case form as some unicode characters do not properly round-trip to * lowercase (such as `ẞ` (German sharp capital s)). */ export function compareStringsCaseInsensitive(a: string, b: string) { if (a === b) return Comparison.EqualTo; if (a === undefined) return Comparison.LessThan; if (b === undefined) return Comparison.GreaterThan; a = a.toUpperCase(); b = b.toUpperCase(); return a < b ? Comparison.LessThan : a > b ? Comparison.GreaterThan : Comparison.EqualTo; } /** * Compare two strings using a case-sensitive ordinal comparison. * * Ordinal comparisons are based on the difference between the unicode code points of both * strings. Characters with multiple unicode representations are considered unequal. Ordinal * comparisons provide predictable ordering, but place "a" after "B". * * Case-sensitive comparisons compare both strings one code-point at a time using the integer * value of each code-point. */ export function compareStringsCaseSensitive(a: string | undefined, b: string | undefined): Comparison { return compareComparableValues(a, b); } export function getStringComparer(ignoreCase?: boolean) { return ignoreCase ? compareStringsCaseInsensitive : compareStringsCaseSensitive; } /** * Creates a string comparer for use with string collation in the UI. */ const createUIStringComparer = (() => { let defaultComparer: Comparer | undefined; let enUSComparer: Comparer | undefined; const stringComparerFactory = getStringComparerFactory(); return createStringComparer; function compareWithCallback(a: string | undefined, b: string | undefined, comparer: (a: string, b: string) => number) { if (a === b) return Comparison.EqualTo; if (a === undefined) return Comparison.LessThan; if (b === undefined) return Comparison.GreaterThan; const value = comparer(a, b); return value < 0 ? Comparison.LessThan : value > 0 ? Comparison.GreaterThan : Comparison.EqualTo; } function createIntlCollatorStringComparer(locale: string | undefined): Comparer { // Intl.Collator.prototype.compare is bound to the collator. See NOTE in // http://www.ecma-international.org/ecma-402/2.0/#sec-Intl.Collator.prototype.compare const comparer = new Intl.Collator(locale, { usage: "sort", sensitivity: "variant" }).compare; return (a, b) => compareWithCallback(a, b, comparer); } function createLocaleCompareStringComparer(locale: string | undefined): Comparer { // if the locale is not the default locale (`undefined`), use the fallback comparer. if (locale !== undefined) return createFallbackStringComparer(); return (a, b) => compareWithCallback(a, b, compareStrings); function compareStrings(a: string, b: string) { return a.localeCompare(b); } } function createFallbackStringComparer(): Comparer { // An ordinal comparison puts "A" after "b", but for the UI we want "A" before "b". // We first sort case insensitively. So "Aaa" will come before "baa". // Then we sort case sensitively, so "aaa" will come before "Aaa". // // For case insensitive comparisons we always map both strings to their // upper-case form as some unicode characters do not properly round-trip to // lowercase (such as `ẞ` (German sharp capital s)). return (a, b) => compareWithCallback(a, b, compareDictionaryOrder); function compareDictionaryOrder(a: string, b: string) { return compareStrings(a.toUpperCase(), b.toUpperCase()) || compareStrings(a, b); } function compareStrings(a: string, b: string) { return a < b ? Comparison.LessThan : a > b ? Comparison.GreaterThan : Comparison.EqualTo; } } function getStringComparerFactory() { // If the host supports Intl, we use it for comparisons using the default locale. if (typeof Intl === "object" && typeof Intl.Collator === "function") { return createIntlCollatorStringComparer; } // If the host does not support Intl, we fall back to localeCompare. // localeCompare in Node v0.10 is just an ordinal comparison, so don't use it. if (typeof String.prototype.localeCompare === "function" && typeof String.prototype.toLocaleUpperCase === "function" && "a".localeCompare("B") < 0) { return createLocaleCompareStringComparer; } // Otherwise, fall back to ordinal comparison: return createFallbackStringComparer; } function createStringComparer(locale: string | undefined) { // Hold onto common string comparers. This avoids constantly reallocating comparers during // tests. if (locale === undefined) { return defaultComparer || (defaultComparer = stringComparerFactory(locale)); } else if (locale === "en-US") { return enUSComparer || (enUSComparer = stringComparerFactory(locale)); } else { return stringComparerFactory(locale); } } })(); let uiComparerCaseSensitive: Comparer | undefined; let uiLocale: string | undefined; export function getUILocale() { return uiLocale; } export function setUILocale(value: string | undefined) { if (uiLocale !== value) { uiLocale = value; uiComparerCaseSensitive = undefined; } } /** * Compare two strings in a using the case-sensitive sort behavior of the UI locale. * * Ordering is not predictable between different host locales, but is best for displaying * ordered data for UI presentation. Characters with multiple unicode representations may * be considered equal. * * Case-sensitive comparisons compare strings that differ in base characters, or * accents/diacritic marks, or case as unequal. */ export function compareStringsCaseSensitiveUI(a: string, b: string) { const comparer = uiComparerCaseSensitive || (uiComparerCaseSensitive = createUIStringComparer(uiLocale)); return comparer(a, b); } export function compareProperties(a: T | undefined, b: T | undefined, key: K, comparer: Comparer): Comparison { return a === b ? Comparison.EqualTo : a === undefined ? Comparison.LessThan : b === undefined ? Comparison.GreaterThan : comparer(a[key], b[key]); } /** True is greater than false. */ export function compareBooleans(a: boolean, b: boolean): Comparison { return compareValues(a ? 1 : 0, b ? 1 : 0); } /** * Given a name and a list of names that are *not* equal to the name, return a spelling suggestion if there is one that is close enough. * Names less than length 3 only check for case-insensitive equality, not Levenshtein distance. * * If there is a candidate that's the same except for case, return that. * If there is a candidate that's within one edit of the name, return that. * Otherwise, return the candidate with the smallest Levenshtein distance, * except for candidates: * * With no name * * Whose length differs from the target name by more than 0.34 of the length of the name. * * Whose levenshtein distance is more than 0.4 of the length of the name * (0.4 allows 1 substitution/transposition for every 5 characters, * and 1 insertion/deletion at 3 characters) */ export function getSpellingSuggestion(name: string, candidates: T[], getName: (candidate: T) => string | undefined): T | undefined { const maximumLengthDifference = Math.min(2, Math.floor(name.length * 0.34)); let bestDistance = Math.floor(name.length * 0.4) + 1; // If the best result isn't better than this, don't bother. let bestCandidate: T | undefined; let justCheckExactMatches = false; const nameLowerCase = name.toLowerCase(); for (const candidate of candidates) { const candidateName = getName(candidate); if (candidateName !== undefined && Math.abs(candidateName.length - nameLowerCase.length) <= maximumLengthDifference) { const candidateNameLowerCase = candidateName.toLowerCase(); if (candidateNameLowerCase === nameLowerCase) { return candidate; } if (justCheckExactMatches) { continue; } if (candidateName.length < 3) { // Don't bother, user would have noticed a 2-character name having an extra character continue; } // Only care about a result better than the best so far. const distance = levenshteinWithMax(nameLowerCase, candidateNameLowerCase, bestDistance - 1); if (distance === undefined) { continue; } if (distance < 3) { justCheckExactMatches = true; bestCandidate = candidate; } else { Debug.assert(distance < bestDistance); // Else `levenshteinWithMax` should return undefined bestDistance = distance; bestCandidate = candidate; } } } return bestCandidate; } function levenshteinWithMax(s1: string, s2: string, max: number): number | undefined { let previous = new Array(s2.length + 1); let current = new Array(s2.length + 1); /** Represents any value > max. We don't care about the particular value. */ const big = max + 1; for (let i = 0; i <= s2.length; i++) { previous[i] = i; } for (let i = 1; i <= s1.length; i++) { const c1 = s1.charCodeAt(i - 1); const minJ = i > max ? i - max : 1; const maxJ = s2.length > max + i ? max + i : s2.length; current[0] = i; /** Smallest value of the matrix in the ith column. */ let colMin = i; for (let j = 1; j < minJ; j++) { current[j] = big; } for (let j = minJ; j <= maxJ; j++) { const dist = c1 === s2.charCodeAt(j - 1) ? previous[j - 1] : Math.min(/*delete*/ previous[j] + 1, /*insert*/ current[j - 1] + 1, /*substitute*/ previous[j - 1] + 2); current[j] = dist; colMin = Math.min(colMin, dist); } for (let j = maxJ + 1; j <= s2.length; j++) { current[j] = big; } if (colMin > max) { // Give up -- everything in this column is > max and it can't get better in future columns. return undefined; } const temp = previous; previous = current; current = temp; } const res = previous[s2.length]; return res > max ? undefined : res; } export function endsWith(str: string, suffix: string): boolean { const expectedPos = str.length - suffix.length; return expectedPos >= 0 && str.indexOf(suffix, expectedPos) === expectedPos; } export function removeSuffix(str: string, suffix: string): string { return endsWith(str, suffix) ? str.slice(0, str.length - suffix.length) : str; } export function tryRemoveSuffix(str: string, suffix: string): string | undefined { return endsWith(str, suffix) ? str.slice(0, str.length - suffix.length) : undefined; } export function stringContains(str: string, substring: string): boolean { return str.indexOf(substring) !== -1; } export function fileExtensionIs(path: string, extension: string): boolean { return path.length > extension.length && endsWith(path, extension); } export function fileExtensionIsOneOf(path: string, extensions: ReadonlyArray): boolean { for (const extension of extensions) { if (fileExtensionIs(path, extension)) { return true; } } return false; } /** * Takes a string like "jquery-min.4.2.3" and returns "jquery" */ export function removeMinAndVersionNumbers(fileName: string) { // Match a "." or "-" followed by a version number or 'min' at the end of the name const trailingMinOrVersion = /[.-]((min)|(\d+(\.\d+)*))$/; // The "min" or version may both be present, in either order, so try applying the above twice. return fileName.replace(trailingMinOrVersion, "").replace(trailingMinOrVersion, ""); } /** Remove an item from an array, moving everything to its right one space left. */ export function orderedRemoveItem(array: T[], item: T): boolean { for (let i = 0; i < array.length; i++) { if (array[i] === item) { orderedRemoveItemAt(array, i); return true; } } return false; } /** Remove an item by index from an array, moving everything to its right one space left. */ export function orderedRemoveItemAt(array: T[], index: number): void { // This seems to be faster than either `array.splice(i, 1)` or `array.copyWithin(i, i+ 1)`. for (let i = index; i < array.length - 1; i++) { array[i] = array[i + 1]; } array.pop(); } export function unorderedRemoveItemAt(array: T[], index: number): void { // Fill in the "hole" left at `index`. array[index] = array[array.length - 1]; array.pop(); } /** Remove the *first* occurrence of `item` from the array. */ export function unorderedRemoveItem(array: T[], item: T) { return unorderedRemoveFirstItemWhere(array, element => element === item); } /** Remove the *first* element satisfying `predicate`. */ function unorderedRemoveFirstItemWhere(array: T[], predicate: (element: T) => boolean) { for (let i = 0; i < array.length; i++) { if (predicate(array[i])) { unorderedRemoveItemAt(array, i); return true; } } return false; } export type GetCanonicalFileName = (fileName: string) => string; export function createGetCanonicalFileName(useCaseSensitiveFileNames: boolean): GetCanonicalFileName { return useCaseSensitiveFileNames ? identity : toLowerCase; } /** Represents a "prefix*suffix" pattern. */ /* @internal */ export interface Pattern { prefix: string; suffix: string; } export function patternText({ prefix, suffix }: Pattern): string { return `${prefix}*${suffix}`; } /** * Given that candidate matches pattern, returns the text matching the '*'. * E.g.: matchedText(tryParsePattern("foo*baz"), "foobarbaz") === "bar" */ export function matchedText(pattern: Pattern, candidate: string): string { Debug.assert(isPatternMatch(pattern, candidate)); return candidate.substring(pattern.prefix.length, candidate.length - pattern.suffix.length); } /** Return the object corresponding to the best pattern to match `candidate`. */ export function findBestPatternMatch(values: ReadonlyArray, getPattern: (value: T) => Pattern, candidate: string): T | undefined { let matchedValue: T | undefined; // use length of prefix as betterness criteria let longestMatchPrefixLength = -1; for (const v of values) { const pattern = getPattern(v); if (isPatternMatch(pattern, candidate) && pattern.prefix.length > longestMatchPrefixLength) { longestMatchPrefixLength = pattern.prefix.length; matchedValue = v; } } return matchedValue; } export function startsWith(str: string, prefix: string): boolean { return str.lastIndexOf(prefix, 0) === 0; } export function removePrefix(str: string, prefix: string): string { return startsWith(str, prefix) ? str.substr(prefix.length) : str; } export function tryRemovePrefix(str: string, prefix: string, getCanonicalFileName: GetCanonicalFileName = identity): string | undefined { return startsWith(getCanonicalFileName(str), getCanonicalFileName(prefix)) ? str.substring(prefix.length) : undefined; } function isPatternMatch({ prefix, suffix }: Pattern, candidate: string) { return candidate.length >= prefix.length + suffix.length && startsWith(candidate, prefix) && endsWith(candidate, suffix); } export function and(f: (arg: T) => boolean, g: (arg: T) => boolean) { return (arg: T) => f(arg) && g(arg); } export function or(f: (arg: T) => boolean, g: (arg: T) => boolean): (arg: T) => boolean { return arg => f(arg) || g(arg); } export function assertTypeIsNever(_: never): void { } // tslint:disable-line no-empty export function singleElementArray(t: T | undefined): T[] | undefined { return t === undefined ? undefined : [t]; } export function enumerateInsertsAndDeletes(newItems: ReadonlyArray, oldItems: ReadonlyArray, comparer: (a: T, b: U) => Comparison, inserted: (newItem: T) => void, deleted: (oldItem: U) => void, unchanged?: (oldItem: U, newItem: T) => void) { unchanged = unchanged || noop; 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 = comparer(newItem, oldItem); if (compareResult === Comparison.LessThan) { inserted(newItem); newIndex++; } else if (compareResult === Comparison.GreaterThan) { deleted(oldItem); oldIndex++; } else { unchanged(oldItem, newItem); newIndex++; oldIndex++; } } while (newIndex < newLen) { inserted(newItems[newIndex++]); } while (oldIndex < oldLen) { deleted(oldItems[oldIndex++]); } } }