alm
Version:
The best IDE for TypeScript
640 lines (569 loc) • 18.2 kB
text/typescript
export function log() {
console.log('log');
}
type MapKey = string|number;
type MapableArray = MapKey[];
export type TruthTable = { [string: string]: boolean;[number: number]: boolean };
/**
* Create a quick lookup map from list
*/
export function createMap(arr: MapableArray): TruthTable {
return arr.reduce((result: { [string: string]: boolean }, key: string) => {
result[key] = true;
return result;
}, <{ [string: string]: boolean }>{});
}
/**
* Create a quick lookup map from list
*/
export function createMapByKey<K extends MapKey,V>(arr: V[], getKey:(item:V)=>K): { [key:string]: V[]; [key:number]: V[]} {
var result: any = {};
arr.forEach(item => {
let key: any = getKey(item);
result[key] = result[key] ? result[key].concat(item) : [item];
})
return result;
}
/**
* Turns keys into values and values into keys
*/
export function reverseKeysAndValues(obj: { [key: string]: string | number, [key: number]: string | number }): { [key: string]: string } {
var toret = {};
Object.keys(obj).forEach(function(key) {
toret[obj[key]] = key;
});
return toret;
}
/** Sloppy but effective code to find distinct */
export function distinct(arr: string[]): string[] {
var map = createMap(arr);
return Object.keys(map);
}
export const uniq = distinct;
/**
* Values for dictionary
*/
export function values<V>(dict: { [key: string]: V;[key: number]: V }): V[] {
return Object.keys(dict).map(key => dict[key]);
}
/**
* Debounce
*/
var now = () => new Date().getTime();
export function debounce<T extends Function>(func: T, milliseconds: number, immediate = false): T {
var timeout, args, context, timestamp, result;
var wait = milliseconds;
var later = function() {
var last = now() - timestamp;
if (last < wait && last > 0) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
if (!immediate) {
result = func.apply(context, args);
if (!timeout) context = args = null;
}
}
};
return <any>function() {
context = this;
args = arguments;
timestamp = now();
var callNow = immediate && !timeout;
if (!timeout) timeout = setTimeout(later, wait);
if (callNow) {
result = func.apply(context, args);
context = args = null;
}
return result;
};
};
/**
* Like debounce but will also call if a state change is significant enough to not ignore silently
* Note:
* - Significant changes : the function is called *immediately* without debouncing (but still marked for future debouncing).
*/
export function triggeredDebounce<Arg>(config:{
func: (arg:Arg)=>void,
/** Only called after there is at least one `oldArg` */
mustcall: (newArg:Arg,oldArg:Arg)=>boolean,
milliseconds: number}): (arg:Arg) => void {
let lastArg, lastCallTimeStamp;
let hasALastArg = false; // true if we are `holding back` any previous arg
let pendingTimeout = null;
const later = function() {
const timeSinceLast = now() - lastCallTimeStamp;
if (timeSinceLast < config.milliseconds) {
if (pendingTimeout) {
clearTimeout(pendingTimeout);
pendingTimeout = null;
}
pendingTimeout = setTimeout(later, config.milliseconds - timeSinceLast);
} else {
config.func(lastArg);
hasALastArg = false;
}
};
return function(arg:Arg) {
const stateChangeSignificant = hasALastArg && config.mustcall(arg,lastArg);
if (stateChangeSignificant) {
config.func(lastArg);
}
lastArg = arg;
hasALastArg = true;
lastCallTimeStamp = now();
later();
};
};
export function throttle<T extends Function>(func: T, milliseconds: number, options?: { leading?: boolean; trailing?: boolean }): T {
var context, args, result;
var timeout = null;
var previous = 0;
if (!options) options = {};
let gnow = now;
var later = function() {
previous = options.leading === false ? 0 : gnow();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function() {
var now = gnow();
if (!previous && options.leading === false) previous = now;
var remaining = milliseconds - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > milliseconds) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
} as any;
};
export function once<T extends Function>(func: T): T {
let ran = false;
let memo = undefined;
return function() {
if (ran) return memo;
ran = true;
memo = func.apply(this, arguments);
func = null;
return memo;
} as any;
}
export function rangeLimited(args: { num: number, min: number, max: number, loopAround?: boolean }) {
let {num, min, max, loopAround} = args;
var limited = Math.max(Math.min(num, max), min);
if (loopAround && limited > num){
return max;
}
if (loopAround && limited < num){
return min;
}
return limited;
}
/**
* Is TypeSript or is JavaScript file checks
*/
export const isTs = (filePath: string, ext = getExt(filePath)) => {
return ext == 'ts' || ext == 'tsx';
}
export const isTsx = (filePath: string, ext = getExt(filePath)) => {
return ext == 'tsx';
}
export const isJs = (filePath: string, ext = getExt(filePath)) => {
return ext == 'js' || ext == 'jsx';
}
export const isJsOrTs = (filePath: string) => {
const ext = getExt(filePath);
return isJs(filePath, ext) || isTs(filePath, ext);
}
/** `/asdf/bar/j.ts` => `ts` */
export function getExt(filePath: string) {
let parts = filePath.split('.');
return parts[parts.length - 1].toLowerCase();
}
/**
* `/asdf/bar/j.ts` => `/asdf/bar/j`
* `/asdf/bar/j.d.ts` => `/asdf/bar/j.d`
*/
export function removeExt(filePath: string) {
const dot = filePath.lastIndexOf('.');
if (dot === -1) {
return filePath;
}
return filePath.substring(0, dot);
}
/**
* asdf/asdf:123 => asdf/asdf + 122
* Note: returned line is 0 based
*/
export function getFilePathLine(query: string) {
let [filePath,lineNum] = query.split(':');
let line = lineNum ? parseInt(lineNum) - 1 : 0;
line = line > 0 ? line : 0;
return { filePath, line };
}
/** `/asdf/bar/j.ts` => `j.ts` */
export function getFileName(fullFilePath:string){
let parts = fullFilePath.split('/');
return parts[parts.length - 1];
}
/** `/asdf/bar/j.ts` => `/asdf/bar` */
export function getDirectory(filePath: string): string {
let directory = filePath.substring(0, filePath.lastIndexOf("/"));
return directory;
}
/** Folder + filename only e.g. `/asdf/something/tsconfig.json` => `something/tsconfig.json` */
export function getDirectoryAndFileName(filePath:string): string {
let directoryPath = getDirectory(filePath);
let directoryName = getFileName(directoryPath);
let fileName = getFileName(filePath);
return `${directoryName}/${fileName}`;
}
/**
* shallow equality of sorted arrays
*/
export function arraysEqual<T>(a: T[], b: T[]): boolean {
if (a === b) return true;
if (a == null || b == null) return false;
if (a.length !== b.length) return false;
for (var i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) return false;
}
return true;
}
/** Creates a Guid (UUID v4) */
export function createId(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
/** Creates a Guid (UUID v4) */
export var createGuid = createId;
// Not optimized
export function selectMany<T>(arr: T[][]): T[] {
var result = [];
for (var i = 0; i < arr.length; i++) {
for (var j = 0; j < arr[i].length; j++) {
result.push(arr[i][j]);
}
}
return result;
}
/** From `file://filePath` to `filePath` */
export function getFilePathFromUrl(url: string) {
let {filePath} = getFilePathAndProtocolFromUrl(url);
return filePath;
}
/** We consistently have tabs with protocol + filePath */
export function getFilePathAndProtocolFromUrl(url: string): {protocol: string; filePath:string}{
// TODO: error handling
let protocol = url.substr(0,url.indexOf('://'));
let filePath = url.substr((protocol + '://').length);
return {protocol, filePath};
}
export function getUrlFromFilePathAndProtocol(config:{protocol:string,filePath:string}){
return config.protocol + '://' + config.filePath;
}
/**
* Promise.resolve is something I call the time (allows you to take x|promise and return promise ... aka make sync prog async if needed)
*/
export var resolve: typeof Promise.resolve = Promise.resolve.bind(Promise);
/** Useful for various editor related stuff e.g. completions */
var punctuations = createMap([';', '{', '}', '(', ')', '.', ':', '<', '>', "'", '"']);
/** Does the prefix end in punctuation */
export var prefixEndsInPunctuation = (prefix: string) => prefix.length && prefix.trim().length && punctuations[prefix.trim()[prefix.trim().length - 1]];
/** String based enum pattern */
export function stringEnum(x){
// make values same as keys
Object.keys(x).map((key) => x[key] = key);
}
/**
* Just adds your intercept function to be called whenever the original function is called
* Calls your function *before* the original is called
*/
export function intercepted<T extends Function>(config: { context: any; orig: T; intercept: T }): T {
return function() {
config.intercept.apply(null, arguments);
return config.orig.apply(config.context, arguments);
} as any;
}
/**
* path.relative for browser
* from : https://github.com/substack/path-browserify/blob/master/index.js
* but modified to not depened on `path.resolve` as from and to are already resolved in our case
*/
export const relative = function(from:string, to:string) {
function trim(arr) {
var start = 0;
for (; start < arr.length; start++) {
if (arr[start] !== '') break;
}
var end = arr.length - 1;
for (; end >= 0; end--) {
if (arr[end] !== '') break;
}
if (start > end) return [];
return arr.slice(start, end - start + 1);
}
var fromParts = trim(from.split('/'));
var toParts = trim(to.split('/'));
var length = Math.min(fromParts.length, toParts.length);
var samePartsLength = length;
for (var i = 0; i < length; i++) {
if (fromParts[i] !== toParts[i]) {
samePartsLength = i;
break;
}
}
var outputParts = [];
for (var i = samePartsLength; i < fromParts.length; i++) {
outputParts.push('..');
}
outputParts = outputParts.concat(toParts.slice(samePartsLength));
return outputParts.join('/');
};
export const imageUrl = '/images';
const supportedImages = {
'svg': 'image/svg+xml',
'gif': 'image/gif',
'png': 'image/png',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'bmp': 'image/bmp'
}
const imageExtensions = Object.keys(supportedImages);
/**
* Provides info on the image files we support
*/
export function isImage(url: string) {
return imageExtensions.some(ext => url.endsWith("." + ext));
}
export function getImageMimeType(filePath: string){
const ext = getExt(filePath);
return supportedImages[ext];
}
/**
* Great for find and replace
*/
export function findOptionsToQueryRegex(options: { query: string, isRegex: boolean, isFullWord: boolean, isCaseSensitive: boolean }): RegExp {
// Note that Code mirror only takes `query` string *tries* to detect case senstivity, regex on its own
// So simpler if we just convert options into regex, and then code mirror will happy use the regex as is
let str = options.query;
var query: RegExp;
/** This came from search.js in code mirror */
let defaultQuery = /x^/;
if (!options.isRegex){
// from CMs search.js
str = str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
}
if (options.isFullWord){
str = `\\b${str}\\b`;
}
try {
query = new RegExp(str, options.isCaseSensitive ? "g" : "gi");
}
catch (e) {
query = defaultQuery;
}
if (query.test("")){
query = defaultQuery;
}
return query;
}
/**
* Quick and dirty pad left
*/
export function padLeft(str: string, paddingValue: number) {
let pad = ' ';
return pad.substring(0, paddingValue - str.length) + str;
}
/**
* Quick and dirty shallow extend
*/
export function extend<A>(a: A): A;
export function extend<A, B>(a: A, b: B): A & B;
export function extend<A, B, C>(a: A, b: B, c: C): A & B & C;
export function extend<A, B, C, D>(a: A, b: B, c: C, d: D): A & B & C & D;
export function extend(...args: any[]): any {
const newObj = {};
for (const obj of args) {
for (const key in obj) {
//copy all the fields
newObj[key] = obj[key];
}
}
return newObj;
};
/**
* Simple timer
*/
export function timer() {
let timeStart = new Date().getTime();
return {
/** <integer>s e.g 2s etc. */
get seconds() {
const seconds = Math.ceil((new Date().getTime() - timeStart) / 1000) + 's';
return seconds;
},
/** Milliseconds e.g. 2000ms etc. */
get ms() {
const ms = (new Date().getTime() - timeStart) + 'ms';
return ms;
}
}
}
/**
* Returns a nice conversion of milliseconds into seconds / mintues as needed
*/
export function formatMilliseconds(ms: number): string {
if (ms < 1000) return `${ms}ms`;
const s = ms / 1000;
if (s < 60) {
return `${s.toFixed(2)}s`;
}
const m = s / 60;
return `${m.toFixed(2)}min`
}
/**
* If you add a new schema make sure you download its schema as well
*/
export const supportedAutocompleteConfigFileNames: { [fileName: string]: boolean } = {
'tsconfig.json': true,
'package.json': true,
'tslint.json': true,
'alm.json': true,
}
/**
* Files for which we have autocomplete intelligence
*/
export function isSupportedConfigFileForAutocomplete(filePath: string): boolean {
const fileName = getFileName(filePath);
return !!supportedAutocompleteConfigFileNames[fileName];
}
export const supportedHoverConfigFileNames: { [fileName: string]: boolean } = {
'package.json': true,
}
/**
* Files for which we have hover intelligence
*/
export function isSupportedConfigFileForHover(filePath: string): boolean {
const fileName = getFileName(filePath);
return !!supportedHoverConfigFileNames[fileName];
}
/**
* Cancellation token
*/
export type CancellationToken = { cancel(): void, isCancelled: boolean };
export const cancellationToken = (): CancellationToken => {
let cancelled = false;
return {
get isCancelled(): boolean {
return cancelled;
},
cancel: () => cancelled = true
}
}
export const cancelled = "cancelled";
/**
* Cancellable For Each
*/
export function cancellableForEach<T>(config: {
items: T[],
cb: (item: T) => void,
cancellationToken: CancellationToken,
}): Promise<any> {
return new Promise((resolve,reject) => {
let index = 0;
const lookAtNext = () => {
// Completed?
if (index === config.items.length) {
resolve({});
}
// Aborted?
else if (config.cancellationToken.isCancelled) {
reject(cancelled);
}
// Next and schedule
else {
const nextItem = config.items[index++];
config.cb(nextItem);
// Yield the thread for a bit
setTimeout(lookAtNext);
}
}
lookAtNext();
});
}
/**
* https://github.com/facebook/react/issues/5465#issuecomment-157888325
*/
const makeCancelable = <T>(promise:Promise<T>) => {
let hasCanceled_ = false;
const wrappedPromise = new Promise((resolve: any, reject) => {
promise.then((val) =>
hasCanceled_ ? reject({isCanceled: true}) : resolve(val)
);
promise.catch((error) =>
hasCanceled_ ? reject({ isCanceled: true }) : reject(error)
);
});
return {
promise: wrappedPromise as Promise<T>,
cancel() {
hasCanceled_ = true;
},
};
};
/**
* For promise chains that might become invalid fast
*/
export function onlyLastCall<T>(call: () => Promise<T>) {
/**
* Kept the delay to 0 as I am using this in select list view
* and that really benifits from immediate feedback.
*/
const delay = 0;
let timeout: any;
let _resolve: any;
let _reject: any;
let _cancel: () => void = () => null;
const later = () => {
timeout = null;
let {promise, cancel} = makeCancelable(call());
_cancel = cancel;
promise
.then((res) => {
_resolve(res);
})
.catch((rej) => {
_reject(rej);
})
}
let trueCall = () => new Promise((resolve, reject) => {
if (timeout) {
clearTimeout(timeout);
}
// Cancel any pending
_cancel();
_resolve = resolve;
_reject = reject;
timeout = setTimeout(later, delay);
})
return trueCall;
}
/**
* Delay given ms
*/
export function delay(ms: number) {
return new Promise(res => setTimeout(res, ms));
}