<commit_before>(function ( $ ) {
    var PLUGIN_IDENTIFIER = 'ytLoad';
    var settings;
    var ajaxError;

    function injectProgressBar() {
        $progressBar = $('#'+settings.progressBarId);

        if ($progressBar.length == 0) {
            $('body').append('

');
            $progressBar = $('#'+settings.progressBarId);
        }

        return $progressBar;
    }

    function completeProgressBar() {
        settings.onComplete();
        $progressBar.remove();
    }

    var methods = {
        init: function(options) {
            ajaxError = false;

            settings = $.extend({
                registerAjaxHandlers: true,
                startPercentage: 30,
                startDuration: 200,
                completeDuration: 50,
                fadeDelay: 200,
                fadeDuration: 200,
                progressBarId: PLUGIN_IDENTIFIER,
                onStart: function() { },
                onComplete: function() { },
                onError: function() { }
            }, options);

            if(settings.registerAjaxHandlers) {
                $(document).on('ajaxStart.'+PLUGIN_IDENTIFIER, function() {
                    methods.start();
                });

                $(document).on('ajaxComplete.'+PLUGIN_IDENTIFIER, function() {
                    if($.active < 2) {
                        methods.complete();
                    }
                });

                $(document).on('ajaxError.'+PLUGIN_IDENTIFIER, function() {
                    methods.error();
                });
            }
        },

        start: function() {
            $progressBar = injectProgressBar();
            ajaxError = false;

            $progressBar.transit({
                width: settings.startPercentage+'%'
            }, settings.startDuration);

            settings.onStart();
        },

        complete: function() {
            $progressBar = injectProgressBar();
            methods.setProgress(100, settings.completeDuration, function() {
                $progressBar.delay(settings.fadeDelay);
                $progressBar.fadeOut({
                    duration: settings.fadeDuration,
                    complete: function() {
                        completeProgressBar();
                    }
                });
            });
        },

        setProgress: function(progress, duration, finished) {
            $progressBar = injectProgressBar();
            if(!duration) {
                duration = progress * 5;
            }
            var width = (101 / 100) * progress;
            width = Math.round(width * 100) / 100;

            $progressBar.transit({
                width: width+'%',
                complete: function() {
                    if(finished) {
                        finished();
                    }
                }
            }, duration);
        },

        error: function() {
            $progressBar = $('#'+settings.progressBarId);
            $progressBar.addClass('error');

            settings.onError();
        },

        destroy: function() {
            if(settings.registerAjaxHandlers) {
                $(document).off('ajaxStart.'+PLUGIN_IDENTIFIER);
                $(document).off('ajaxComplete.'+PLUGIN_IDENTIFIER);
                $(document).off('ajaxError.'+PLUGIN_IDENTIFIER);
            }
        }
    };

    $.ytLoad = function(methodOrOptions) {
        if ( methods[methodOrOptions] ) {
            return methods[ methodOrOptions ].apply( this, Array.prototype.slice.call( arguments, 1 ));
        } else if ( typeof methodOrOptions === 'object' || ! methodOrOptions ) {
            return methods.init.apply( this, arguments );
        } else {
            $.error( 'Method ' +  method + ' does not exist on jQuery.'+PLUGIN_IDENTIFIER );
        }
    };
}( jQuery ));<commit_msg>setProgress implemented<commit_after>(function ( $ ) {
    var PLUGIN_IDENTIFIER = 'ytLoad';
    var settings;
    var ajaxError;

    function injectProgressBar() {
        var $progressBar = $('#'+settings.progressBarId);

        if ($progressBar.length == 0) {
            $('body').append('

');
            $progressBar = $('#'+settings.progressBarId);
        }

        return $progressBar;
    }

    function completeProgressBar() {
        var $progressBar = $('#'+settings.progressBarId);

        $progressBar.remove();
        settings.onComplete();
    }

    var methods = {
        init: function(options) {
            ajaxError = false;

            settings = $.extend({
                registerAjaxHandlers: true,
                startPercentage: 30,
                startDuration: null,
                completeDuration: 100,
                fadeDelay: 200,
                fadeDuration: 200,
                progressBarId: PLUGIN_IDENTIFIER,
                onStart: function() { },
                onComplete: function() { },
                onError: function() { }
            }, options);

            if(settings.registerAjaxHandlers) {
                $(document).on('ajaxStart.'+PLUGIN_IDENTIFIER, function() {
                    methods.start();
                });

                $(document).on('ajaxComplete.'+PLUGIN_IDENTIFIER, function() {
                    if($.active < 2) {
                        methods.complete();
                    }
                });

                $(document).on('ajaxError.'+PLUGIN_IDENTIFIER, function() {
                    methods.error();
                });
            }
        },

        start: function() {
            var $progressBar = injectProgressBar();
            ajaxError = false;

            methods.setProgress(settings.startPercentage, settings.startDuration);
            settings.onStart();
        },

        complete: function() {
            var $progressBar = injectProgressBar();
            methods.setProgress(100, settings.completeDuration, function() {
                $progressBar.delay(settings.fadeDelay);
                $progressBar.fadeOut({
                    duration: settings.fadeDuration
                });
            });
        },

        setProgress: function(progress, duration, finished) {
            var $progressBar = injectProgressBar();
            if(!duration) {
                duration = progress * 5;
            }
            var width = (101 / 100) * progress;
            width = Math.round(width * 100) / 100;

            if(width > 99) {
                // TODO: Bug in jquery.transit?
                var doubleCompleteHack = false;
            }

            $progressBar.transit({
                width: width+'%',
                duration: duration,
                complete: function() {
                    if(width > 99) {
                        if(doubleCompleteHack == true) {
                            completeProgressBar();
                        }
                        doubleCompleteHack = true;
                    }
                    if(finished) {
                        finished();
                    }
                }
            });
        },

        error: function() {
            var $progressBar = $('#'+settings.progressBarId);
            $progressBar.addClass('error');

            settings.onError();
        },

        destroy: function() {
            if(settings.registerAjaxHandlers) {
                $(document).off('ajaxStart.'+PLUGIN_IDENTIFIER);
                $(document).off('ajaxComplete.'+PLUGIN_IDENTIFIER);
                $(document).off('ajaxError.'+PLUGIN_IDENTIFIER);
            }
        }
    };

    $.ytLoad = function(methodOrOptions) {
        if ( methods[methodOrOptions] ) {
            return methods[ methodOrOptions ].apply( this, Array.prototype.slice.call( arguments, 1 ));
        } else if ( typeof methodOrOptions === 'object' || ! methodOrOptions ) {
            return methods.init.apply( this, arguments );
        } else {
            $.error( 'Method ' +  method + ' does not exist on jQuery.'+PLUGIN_IDENTIFIER );
        }
    };
}( jQuery ));<|endoftext|>


<commit_before>const fs = require('fs');
const path = require('path');
const merge = require('webpack-merge');
const idify = require('html4-id');
const xmldom = require('xmldom');

module.exports = (files = [], options = {}) => {
    options = merge({
        gutter: 0,
        prefix: '',
        generate: {
            title: true,
            symbol: true,
            use: false,
            view: false
        }
    }, options);

    // No point in generating when there are no files
    if ( !files.length ) {
        return;
    }

    // Initialize DOM/XML
    const DOMParser = new xmldom.DOMParser();
    const XMLSerializer = new xmldom.XMLSerializer();
    const XMLDoc = new xmldom.DOMImplementation().createDocument(null, null, null);

    // Create SVG element
    const svg = XMLDoc.createElement('svg');
    const sizes = {
        width: [],
        height: []
    };

    const formatPostfix = (value) => {
        if ( typeof value === 'string' ) {
            return value;
        }

        return '';
    };

    // Add namespaces
    svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
    if ( options.generate.use ) {
        svg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
    }

    // Add elements for each file
    files.forEach((file) => {
        const id = `${options.prefix}${idify(path.basename(file, path.extname(file)))}`;

        // Parse source SVG
        const contents = fs.readFileSync(file, 'utf8');
        const sprite = DOMParser.parseFromString(contents).documentElement;

        // Get sizes
        let viewbox = (sprite.getAttribute('viewBox') || sprite.getAttribute('viewbox')).split(' ').map((a) => parseFloat(a));
        let width = parseFloat(sprite.getAttribute('width'));
        let height = parseFloat(sprite.getAttribute('height'));

        if ( viewbox.length !== 4 && ( isNaN(width) || isNaN(height) ) ) {
            throw new Error(`Invalid SVG '${file}'; it's lacking both a viewBox and width/height attributes...`);
        }

        if ( viewbox.length !== 4 ) {
            viewbox = [0, 0, width, height];
        }

        if ( isNaN(width) ) {
            width = viewbox[2];
        }

        if ( isNaN(height) ) {
            height = viewbox[3];
        }

        // Create symbol
        if ( options.generate.symbol ) {
            const symbol = XMLDoc.createElement('symbol');
            symbol.setAttribute('id', `${id}${formatPostfix(options.generate.symbol)}`);
            symbol.setAttribute('viewBox', viewbox.join(' '));

            if ( options.generate.title ) {
                // Make sure we don't overwrite the existing title
                const hasTitle = (sprite) => {
                    const titles = Array.from(sprite.childNodes).filter((childNode) => {
                        return childNode.nodeName.toLowerCase() === 'title';
                    });

                    return !!titles.length;
                };

                // Add title to improve accessibility
                if ( !hasTitle(sprite) ) {
                    const title = XMLDoc.createElement('title');
                    title.appendChild(XMLDoc.createTextNode(id.replace(options.prefix, '')));
                    symbol.appendChild(title);
                }
            }

            // Clone the original contents of the SVG file into the new symbol
            Array.from(sprite.childNodes).forEach((childNode) => {
                symbol.appendChild(childNode);
            });

            svg.appendChild(symbol);
        }

        if ( options.generate.use ) {
            // Generate <use> elements within spritemap to allow usage within CSS
            const use = XMLDoc.createElement('use');
            const y = sizes.height.reduce((a, b) => a + b, 0) + (sizes.height.length * options.gutter);

            use.setAttribute('xlink:href', `#${id}${formatPostfix(options.generate.symbol)}`);
            use.setAttribute('x', '0');
            use.setAttribute('y', y);
            use.setAttribute('width', width.toString());
            use.setAttribute('height', height.toString());
            svg.appendChild(use);
        }

        if ( options.generate.view ) {
            // Generate <view> elements within spritemap to allow usage within CSS
            const view = XMLDoc.createElement('view');
            const y = sizes.height.reduce((a, b) => a + b, 0) + (sizes.height.length * options.gutter);

            view.setAttribute('id', `${id}${formatPostfix(options.generate.view)}`);
            view.setAttribute('viewBox', `0 ${Math.max(0, y - (options.gutter / 2))} ${width + (options.gutter / 2)} ${y + height + (options.gutter / 2)}`);
            svg.appendChild(view);
        }

        // Update sizes
        sizes.width.push(width);
        sizes.height.push(height);
    });

    if ( options.generate.use ) {
        // Add width/height to spritemap
        svg.setAttribute('width', Math.max.apply(null, sizes.width).toString());
        svg.setAttribute('height', (sizes.height.reduce((a, b) => a + b, 0) + ((sizes.height.length - 1) * options.gutter)).toString());
    }

    return XMLSerializer.serializeToString(svg);
};
<commit_msg>Fixes <view> element viewBox height property<commit_after>const fs = require('fs');
const path = require('path');
const merge = require('webpack-merge');
const idify = require('html4-id');
const xmldom = require('xmldom');

module.exports = (files = [], options = {}) => {
    options = merge({
        gutter: 0,
        prefix: '',
        generate: {
            title: true,
            symbol: true,
            use: false,
            view: false
        }
    }, options);

    // No point in generating when there are no files
    if ( !files.length ) {
        return;
    }

    // Initialize DOM/XML
    const DOMParser = new xmldom.DOMParser();
    const XMLSerializer = new xmldom.XMLSerializer();
    const XMLDoc = new xmldom.DOMImplementation().createDocument(null, null, null);

    // Create SVG element
    const svg = XMLDoc.createElement('svg');
    const sizes = {
        width: [],
        height: []
    };

    const formatPostfix = (value) => {
        if ( typeof value === 'string' ) {
            return value;
        }

        return '';
    };

    // Add namespaces
    svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
    if ( options.generate.use ) {
        svg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
    }

    // Add elements for each file
    files.forEach((file) => {
        const id = `${options.prefix}${idify(path.basename(file, path.extname(file)))}`;

        // Parse source SVG
        const contents = fs.readFileSync(file, 'utf8');
        const sprite = DOMParser.parseFromString(contents).documentElement;

        // Get sizes
        let viewbox = (sprite.getAttribute('viewBox') || sprite.getAttribute('viewbox')).split(' ').map((a) => parseFloat(a));
        let width = parseFloat(sprite.getAttribute('width'));
        let height = parseFloat(sprite.getAttribute('height'));

        if ( viewbox.length !== 4 && ( isNaN(width) || isNaN(height) ) ) {
            throw new Error(`Invalid SVG '${file}'; it's lacking both a viewBox and width/height attributes...`);
        }

        if ( viewbox.length !== 4 ) {
            viewbox = [0, 0, width, height];
        }

        if ( isNaN(width) ) {
            width = viewbox[2];
        }

        if ( isNaN(height) ) {
            height = viewbox[3];
        }

        // Create symbol
        if ( options.generate.symbol ) {
            const symbol = XMLDoc.createElement('symbol');
            symbol.setAttribute('id', `${id}${formatPostfix(options.generate.symbol)}`);
            symbol.setAttribute('viewBox', viewbox.join(' '));

            if ( options.generate.title ) {
                // Make sure we don't overwrite the existing title
                const hasTitle = (sprite) => {
                    const titles = Array.from(sprite.childNodes).filter((childNode) => {
                        return childNode.nodeName.toLowerCase() === 'title';
                    });

                    return !!titles.length;
                };

                // Add title to improve accessibility
                if ( !hasTitle(sprite) ) {
                    const title = XMLDoc.createElement('title');
                    title.appendChild(XMLDoc.createTextNode(id.replace(options.prefix, '')));
                    symbol.appendChild(title);
                }
            }

            // Clone the original contents of the SVG file into the new symbol
            Array.from(sprite.childNodes).forEach((childNode) => {
                symbol.appendChild(childNode);
            });

            svg.appendChild(symbol);
        }

        if ( options.generate.use ) {
            // Generate <use> elements within spritemap to allow usage within CSS
            const use = XMLDoc.createElement('use');
            const y = sizes.height.reduce((a, b) => a + b, 0) + (sizes.height.length * options.gutter);

            use.setAttribute('xlink:href', `#${id}${formatPostfix(options.generate.symbol)}`);
            use.setAttribute('x', '0');
            use.setAttribute('y', y);
            use.setAttribute('width', width.toString());
            use.setAttribute('height', height.toString());
            svg.appendChild(use);
        }

        if ( options.generate.view ) {
            // Generate <view> elements within spritemap to allow usage within CSS
            const view = XMLDoc.createElement('view');
            const y = sizes.height.reduce((a, b) => a + b, 0) + (sizes.height.length * options.gutter);

            view.setAttribute('id', `${id}${formatPostfix(options.generate.view)}`);
            view.setAttribute('viewBox', `0 ${Math.max(0, y - (options.gutter / 2))} ${width + (options.gutter / 2)} ${height + (options.gutter / 2)}`);
            svg.appendChild(view);
        }

        // Update sizes
        sizes.width.push(width);
        sizes.height.push(height);
    });

    if ( options.generate.use ) {
        // Add width/height to spritemap
        svg.setAttribute('width', Math.max.apply(null, sizes.width).toString());
        svg.setAttribute('height', (sizes.height.reduce((a, b) => a + b, 0) + ((sizes.height.length - 1) * options.gutter)).toString());
    }

    return XMLSerializer.serializeToString(svg);
};
<|endoftext|>


<commit_before>/*
 * grunt
 * https://github.com/cowboy/grunt
 *
 * Copyright (c) 2012 "Cowboy" Ben Alman
 * Licensed under the MIT license.
 * http://benalman.com/about/license/
 */

// The actual config data.
var data;

// Recursively expand config directives.
function expandConfig(value) {
  var expandable = ['config', 'json'];
  var parts;
  if (value instanceof Array) {
    // If value is an array, recurse.
    return value.map(expandConfig);
  } else if (typeof value === 'string') {
    // If value is an expandable directive, expand it if possible.
    parts = task.getDirectiveParts(value) || [];
    return expandable.indexOf(parts[0]) !== -1 ? task.directive(value) : value;
  } else {
    // Neither array or string. Just return the value.
    return value;
  }
}

// If prop is an array, convert it to a props string.
function getPropString(prop) {
  if (util.kindOf(prop) === 'array') {
    return prop.map(exports.escape).join('.');
  }
  return prop;
}

// Get/set config data. If data hasn't been set, return null. If value was
// passed, set value. If props string wasn't passed, return all data. Otherwise,
// return the prop's value (execute as a directive, when necessary).
exports = module.exports = function(prop, value) {
  // Abort if no config data exists.
  if (!data) { return null; }
  // If prop is an array, convert it to a prop string.
  prop = getPropString(prop);

  if (arguments.length === 2) {
    // Two arguments were passed, set the property's value.
    return util.namespace.set(data, prop, value);
  } else if (prop) {
    // A property string/array was passed, get that property's value.
    return expandConfig(util.namespace.get(data, prop));
  } else {
    // Nothing was passed. Return the actual config data, with any expandable
    // directives expanded (shallow, for now).
    value = {};
    Object.keys(data).forEach(function(key) {
      value[key] = expandConfig(data[key]);
    });
    return value;
  }
};

// Has config been initialized?
exports.initialized = function() {
  return !!data;
};

// Initialize config data.
exports.init = function(obj) {
  return (data = obj || {});
};

// Escape any . in name with \. so dot-based namespacing works properly.
exports.escape = function(str) {
  return str.replace(/\./g, '\\.');
};

// Test to see if required config params have been defined. If not, throw an
// exception (use this inside of a task).
exports.requires = function() {
  var props = util.toArray(arguments).map(getPropString);
  var msg = 'Verifying option' + (props.length === 1 ? '' : 's') +
    ' ' + log.wordlist(props) + ' exist' + (props.length === 1 ? 's' : '') +
    ' in config...';
  verbose.write(msg);
  var initialized = exports.initialized();
  var failProps = initialized && props.filter(function(prop) {
    return !exports(prop);
  }).map(function(prop) {
    return '"' + prop + '"';
  });
  if (initialized && failProps.length === 0) {
    verbose.ok();
    return true;
  } else {
    verbose.or.write(msg);
    log.error().error('Unable to process task.');
    if (!initialized) {
      throw task.taskError('Unable to load config.');
    } else {
      throw task.taskError('Required config propert' +
        (failProps.length === 1 ? 'y' : 'ies') + ' ' + failProps.join(', ') +
        ' missing.');
    }
  }
};
<commit_msg>If a config prop is falsy but not undefined, it shouldn't error!<commit_after>/*
 * grunt
 * https://github.com/cowboy/grunt
 *
 * Copyright (c) 2012 "Cowboy" Ben Alman
 * Licensed under the MIT license.
 * http://benalman.com/about/license/
 */

// The actual config data.
var data;

// Recursively expand config directives.
function expandConfig(value) {
  var expandable = ['config', 'json'];
  var parts;
  if (value instanceof Array) {
    // If value is an array, recurse.
    return value.map(expandConfig);
  } else if (typeof value === 'string') {
    // If value is an expandable directive, expand it if possible.
    parts = task.getDirectiveParts(value) || [];
    return expandable.indexOf(parts[0]) !== -1 ? task.directive(value) : value;
  } else {
    // Neither array or string. Just return the value.
    return value;
  }
}

// If prop is an array, convert it to a props string.
function getPropString(prop) {
  if (util.kindOf(prop) === 'array') {
    return prop.map(exports.escape).join('.');
  }
  return prop;
}

// Get/set config data. If data hasn't been set, return null. If value was
// passed, set value. If props string wasn't passed, return all data. Otherwise,
// return the prop's value (execute as a directive, when necessary).
exports = module.exports = function(prop, value) {
  // Abort if no config data exists.
  if (!data) { return null; }
  // If prop is an array, convert it to a prop string.
  prop = getPropString(prop);

  if (arguments.length === 2) {
    // Two arguments were passed, set the property's value.
    return util.namespace.set(data, prop, value);
  } else if (prop) {
    // A property string/array was passed, get that property's value.
    return expandConfig(util.namespace.get(data, prop));
  } else {
    // Nothing was passed. Return the actual config data, with any expandable
    // directives expanded (shallow, for now).
    value = {};
    Object.keys(data).forEach(function(key) {
      value[key] = expandConfig(data[key]);
    });
    return value;
  }
};

// Has config been initialized?
exports.initialized = function() {
  return !!data;
};

// Initialize config data.
exports.init = function(obj) {
  return (data = obj || {});
};

// Escape any . in name with \. so dot-based namespacing works properly.
exports.escape = function(str) {
  return str.replace(/\./g, '\\.');
};

// Test to see if required config params have been defined. If not, throw an
// exception (use this inside of a task).
exports.requires = function() {
  var props = util.toArray(arguments).map(getPropString);
  var msg = 'Verifying option' + (props.length === 1 ? '' : 's') +
    ' ' + log.wordlist(props) + ' exist' + (props.length === 1 ? 's' : '') +
    ' in config...';
  verbose.write(msg);
  var initialized = exports.initialized();
  var failProps = initialized && props.filter(function(prop) {
    return exports(prop) === undefined;
  }).map(function(prop) {
    return '"' + prop + '"';
  });
  if (initialized && failProps.length === 0) {
    verbose.ok();
    return true;
  } else {
    verbose.or.write(msg);
    log.error().error('Unable to process task.');
    if (!initialized) {
      throw task.taskError('Unable to load config.');
    } else {
      throw task.taskError('Required config propert' +
        (failProps.length === 1 ? 'y' : 'ies') + ' ' + failProps.join(', ') +
        ' missing.');
    }
  }
};
<|endoftext|>


<commit_before>

/** @return true if the device is iPad or iPad simulator
 */
function isPad() {
    return UIATarget.localTarget().model().indexOf("iPad")>=0;
}

var Direction = {
    Up    : {value:0, name:"Up"},
    Down  : {value:1, name:"Down"},
    Left  : {value:3, name:"Left"},
    Right : {value:4, name:"Right"}
};

var Check = "check";
var Tap = "tap";
var TryTap = "tryTap";
var Investigate = "investigate";
var Input = "input";
var Wait = "wait";
var Scroll = "scroll";
var Swipe = "swipe";
var TapPoint = "tapPoint";
var WaitFor = "waitFor";
var Pick = "pick";

var GlobalEnv = {
    Pass : true,
};

var DefaultMaxWaitingTime = 10;

/// Shortcut for debug log
function log(str) {
	UIALogger.logDebug(str);
}

/// short cut for getting main window
function win() {
	return UIATarget.localTarget().frontMostApp().mainWindow();
}

/// short cut for delay
function wait(i) {
	UIATarget.localTarget().delay(i);
}

/** Checks if 2 elements overlap
 */
function overlap(element1, element2) {
    var rect1 = element1.rect();
    var rect2 = element2.rect();
    return (rect1.origin.x + rect1.size.width > rect2.origin.x && rect2.origin.x + rect2.size.width > rect1.origin.x
            && rect1.origin.y + rect1.size.height > rect2.origin.y && rect2.origin.y + rect2.size.height > rect1.origin.y);
}

/** return the center point of a rect, a dict with x and y
 */
function centerPoint(r) {
    return {x: (r.origin.x+r.size.width/2), y: (r.origin.y+r.size.height/2)};
}

/** return the screen center point, a dict with x and y
 */
function screenCenter() {
    var size = win().rect().size;
    return {x: size.width/2, y: size.height/2};
}

/** check if an element is inside screen
 */
function inScreen(element) {
    if (!element || !(element instanceof UIAElement)) return false;
    var size = element.rect().size;
	return overlap(element, win()) && size.width>0 && size.height>0;
}

/** converts a rect object to a string
 */
function rect2str(r) {
    return "(" + r.origin.x + ", " + r.origin.y + ") w=" + r.size.width + ", h="+ r.size.height;
}

/** Get an array of children for an element
 */
function childrenArray(element) {
    var children = (element instanceof UIATableView)? element.visibleCells() : element.elements();
    var array = children.toArray();
    if (element instanceof UIATableView) {
        if (element.groups()) {
            array = array.concat(element.groups().toArray());
        }
    }
    return array;
}

/** Get the debug text of an element
 */
function debugText(element) {
    var text = "[" + element.name() + "] " + element + " @ " + rect2str(element.rect());
    return text;
}

/** Mark the result as passed or failed in instruments
 */
function displayResult() {
    if (GlobalEnv.Pass) {
        UIALogger.logPass("Passed all tests");
    }
    else {
        UIALogger.logFail("Failed");
    }
}

/// Safer than built-in logElementTree since this doesn't do recursion
function logChildren(element) {
    var array = childrenArray(element);
    for (var i in array) {
        var element = array[i];
        var prefix = inScreen(element)? "√ " : "X "
        log( prefix + debugText(element) );
    }
}

/// Safer than built-in logElementTree since this does recursion more conservatively
function logTree(element) {
    var stack = [element];
    var levelStack = [0];
    while (stack.length) {
        var current = stack.pop();
        var currentLevel = levelStack.pop();
        log(Array(currentLevel).join("- ") + debugText(current));
        var children = childrenArray(current);
        for (var i in children.reverse()) {
            var child = children[i];
            if (inScreen(child) && !(child instanceof UIAApplication) ) {
                var childLevel = currentLevel + 1;
                levelStack.push(childLevel)
                stack.push(child);
            }
        }
    }
}

/** Swipe at a point to a direction
 * point is a dictionary with keys: x and y
 * direction is an instance of Direction enum
 */
function swipe(point, direction, distance, duration) {

    if (!distance) {
        distance = isPad() ? 300 : 100;
    }

    var humanFingerChubbiness = 5;
    var to = {};
    to.x = point.x;
    to.y = point.y;
    if (direction == Direction.Up) {
        to.y = to.y - distance;
        to.x = to.x + humanFingerChubbiness;
    }
    else if (direction == Direction.Down) {
        to.y = to.y + distance;
        to.x = to.x + humanFingerChubbiness;
    }
    else if (direction == Direction.Left) {
        to.x = to.x - distance;
        to.y = to.y + humanFingerChubbiness;
    }
    else if (direction == Direction.Right) {
        to.x = to.x + distance;
        to.y = to.y + humanFingerChubbiness;
    }

    if (!duration) {
        UIATarget.localTarget().flickFromTo(point, to);
    }
    else {
        UIATarget.localTarget().dragFromToForDuration(point, to, duration);
    }
    wait(2);
}

/// Find an element whose name contains str within element
function findChild(element, str, type) {
    var splitIdx = str.indexOf("::");
    if (splitIdx>=0) {
        var items = str.split("::");
        var first = findChild(element, items[0]);
        if (!first) return null;
        return findChild(first, str.substring(splitIdx+2));
    }

    var realtype = type;
    var realstr = str;

    var reg = /^\(\((.*)\)\)(.*)/;
    var rmatch = reg.exec(str);
    if (rmatch) {
        realtype = rmatch[1];
        realstr = rmatch[2];
    }

    var skipCount = 0;
    var reg2 = /(.*)\[\[(\d+)\]\]$/;
    var rmatch2 = reg2.exec(realstr);
    if (rmatch2) {
        realstr = rmatch2[1];
        skipCount = parseInt(rmatch2[2]);
    }

    var array = childrenArray(element);
    for (var i in array) {
        var element = array[i];
        if (!inScreen(element)) {
            continue;
        }
        var match = true;
        if (realstr.length) {
            if (!element.name()) {
                match = false;
            }
            else if (element.name().indexOf(realstr)<0) {
                match = false;
            }
        }
        if (realtype) {
            var info = " "+element;
            if (info.indexOf(realtype)<0) {
                match = false;
            }
        }
        if (match) {
            if (skipCount>0) {
                skipCount --;
            }
            else {
                return element;
            }
        }
    }
    return null;
}

/// Current time in seconds
function currentTimeInSec() {
    return (new Date().getTime() / 1000);
}

/// Allowing a maximum waiting time, and check whether the target element appears
/// Returns the element if it exists.
function waitForElement(target, maxTime) {
    var window = win();
    var child = findChild(window, target);
    var timeStarted = currentTimeInSec();
    var timeSpent = 0;
    while(!child && timeSpent<maxTime) {
        wait(1);
        child = findChild(window, target);
        timeSpent = currentTimeInSec() - timeStarted;
    }
    return child;
}

/// Perform task based on task array
function performTask(tasks) {
    if (!GlobalEnv.Pass) {
        return;
    }

    for (var i in tasks) {
        wait(1); // be tolerant at the beginning

        var step  = tasks[i];
        log("performing "+step);

        var missing = performStep(step);

        if (missing) {
            break;
        }
    }
    if (missing) {
        log("[error] cannot find "+ missing + " during step " + step);
        UIALogger.logFail("fail to find "+ missing + " during step " + step);
        // throw "fail";
        GlobalEnv.Pass = false;
    }
}

function performStep(step) {
    var window = win();
    var action = step[0];
    var missing = null;
    var rescueIdx = -1;

    if (Check == action) {
        waitForElement(step[1], DefaultMaxWaitingTime);
        for (var j in step) {
            if (j>0) {
                if (!findChild(window, step[j])) {
                    missing = step[j];
                    break;
                }
            }
        }

    }
    else if (Tap == action || TryTap == action) {
        waitForElement(step[1], DefaultMaxWaitingTime);
        var b = findChild(window, step[1]);
        if (!b) {
            missing = step[1];
        }
        else {
            try {
                // log("going to tap " + b + b.name() + rect2str(b.rect()) );
                if (TryTap ==action) {
                    UIATarget.localTarget().tap(centerPoint(b.rect()));
                }
                else if (b instanceof UIAButton) {
                    b.tap();
                }
                else {
                    UIATarget.localTarget().tap(centerPoint(b.rect()));
                }
            }
            catch(ex) {
                missing = step[1];
            }
        }
        if (action== Tap) {
            rescueIdx = 2;
        }
        else { // TryTap
            if (missing) {
                log("Failed to tap "+missing+" but it is fine");
            }
            missing = null; // it's fine
        }
    }
    else if (Investigate == action) {
        if (step.length>1) {
            var e = findChild(window, step[1]);
            if (!e) {
                missing = step[1];
            }
            else {
                logTree(e);
            }
        }
        else {
            logTree(window);
            // UIATarget.localTarget().logElementTree();
        }
    }
    else if (Input == action) {
        var kb = UIATarget.localTarget().frontMostApp().keyboard();
        if (kb) {
            kb.typeString(step[1]);
        }
        else {
            missing = "Keyboard";
            rescueIdx = 2;
        }
    }
    else if (Wait == action) {
        wait(step[1]);
    }
    else if (Scroll == action) {
        var point = screenCenter();
        if (step[2]) {
            var element = findChild(window, step[2]);
            if (element) {
                point = centerPoint(element.rect());
            }
        }
        swipe(point, step[1], null, 1);
    }
    else if (Swipe == action) {
        var point = screenCenter();
        if (step[2]) {
            var element = findChild(window, step[2]);
            if (element) {
                point = centerPoint(element.rect());
            }
        }
        swipe(point, step[1]);
    }
    else if (TapPoint == action) {
        UIATarget.localTarget().tap(step[1]);
    }
    else if (WaitFor == action) {
        var target = step[1];
        var maxTime = step[2];
        var child = waitForElement(target, maxTime);
        if (!child) {
            missing = target;
            rescueIdx = 3;
        }
    }
    else if (Pick == action) {
        waitForElement(step[1], DefaultMaxWaitingTime);
        var picker = findChild(window, step[1]);
        if (!picker || !(picker instanceof UIAPickerWheel) ) {
            missing = step[1];
        }
        else {
            try {
                picker.selectValue(step[2]);
            }
            catch(ex) {
                missing = step[2];
            }
        }
        rescueIdx = 3;
    }

    // see if we can rescue
    if (missing && rescueIdx && step[rescueIdx]) {
        rescueTask = step[rescueIdx];
        step.splice(rescueIdx,1);
        wait(1);
        log("rescuing with "+rescueTask);
        var rescueMissing = performStep(rescueTask);
        if (!rescueMissing) {
            wait(1);
            log("retrying "+step);
            missing = performStep(step);
        }
    }

    return missing;
}

<commit_msg>Made the investigate action's result more readable<commit_after>

/** @return true if the device is iPad or iPad simulator
 */
function isPad() {
    return UIATarget.localTarget().model().indexOf("iPad")>=0;
}

var Direction = {
    Up    : {value:0, name:"Up"},
    Down  : {value:1, name:"Down"},
    Left  : {value:3, name:"Left"},
    Right : {value:4, name:"Right"}
};

var Check = "check";
var Tap = "tap";
var TryTap = "tryTap";
var Investigate = "investigate";
var Input = "input";
var Wait = "wait";
var Scroll = "scroll";
var Swipe = "swipe";
var TapPoint = "tapPoint";
var WaitFor = "waitFor";
var Pick = "pick";

var GlobalEnv = {
    Pass : true,
};

var DefaultMaxWaitingTime = 10;

/// Shortcut for debug log
function log(str) {
	UIALogger.logDebug(str);
}

/// short cut for getting main window
function win() {
	return UIATarget.localTarget().frontMostApp().mainWindow();
}

/// short cut for delay
function wait(i) {
	UIATarget.localTarget().delay(i);
}

/** Checks if 2 elements overlap
 */
function overlap(element1, element2) {
    var rect1 = element1.rect();
    var rect2 = element2.rect();
    return (rect1.origin.x + rect1.size.width > rect2.origin.x && rect2.origin.x + rect2.size.width > rect1.origin.x
            && rect1.origin.y + rect1.size.height > rect2.origin.y && rect2.origin.y + rect2.size.height > rect1.origin.y);
}

/** return the center point of a rect, a dict with x and y
 */
function centerPoint(r) {
    return {x: (r.origin.x+r.size.width/2), y: (r.origin.y+r.size.height/2)};
}

/** return the screen center point, a dict with x and y
 */
function screenCenter() {
    var size = win().rect().size;
    return {x: size.width/2, y: size.height/2};
}

/** check if an element is inside screen
 */
function inScreen(element) {
    if (!element || !(element instanceof UIAElement)) return false;
    var size = element.rect().size;
	return overlap(element, win()) && size.width>0 && size.height>0;
}

/** converts a rect object to a string
 */
function rect2str(r) {
    return "(" + r.origin.x + ", " + r.origin.y + ") w=" + r.size.width + ", h="+ r.size.height;
}

/** Get an array of children for an element
 */
function childrenArray(element) {
    var children = (element instanceof UIATableView)? element.visibleCells() : element.elements();
    var array = children.toArray();
    if (element instanceof UIATableView) {
        if (element.groups()) {
            array = array.concat(element.groups().toArray());
        }
    }
    return array;
}

/** Get the debug text of an element
 */
function debugText(element) {
    var text = "[" + element.name() + "] " + element + " @ " + rect2str(element.rect());
    return text;
}

/** Mark the result as passed or failed in instruments
 */
function displayResult() {
    if (GlobalEnv.Pass) {
        UIALogger.logPass("Passed all tests");
    }
    else {
        UIALogger.logFail("Failed");
    }
}

/// Safer than built-in logElementTree since this doesn't do recursion
function logChildren(element) {
    var array = childrenArray(element);
    for (var i in array) {
        var element = array[i];
        var prefix = inScreen(element)? "√ " : "X "
        log( prefix + debugText(element) );
    }
}

/// Safer than built-in logElementTree since this does recursion more conservatively
function logTree(element) {
    var stack = [element];
    var levelStack = [0];
    while (stack.length) {
        var current = stack.pop();
        var currentLevel = levelStack.pop();
        log("* " + Array(currentLevel).join("- ") + debugText(current));
        var children = childrenArray(current);
        for (var i in children.reverse()) {
            var child = children[i];
            if (inScreen(child) && !(child instanceof UIAApplication) ) {
                var childLevel = currentLevel + 1;
                levelStack.push(childLevel)
                stack.push(child);
            }
        }
    }
}

/** Swipe at a point to a direction
 * point is a dictionary with keys: x and y
 * direction is an instance of Direction enum
 */
function swipe(point, direction, distance, duration) {

    if (!distance) {
        distance = isPad() ? 300 : 100;
    }

    var humanFingerChubbiness = 5;
    var to = {};
    to.x = point.x;
    to.y = point.y;
    if (direction == Direction.Up) {
        to.y = to.y - distance;
        to.x = to.x + humanFingerChubbiness;
    }
    else if (direction == Direction.Down) {
        to.y = to.y + distance;
        to.x = to.x + humanFingerChubbiness;
    }
    else if (direction == Direction.Left) {
        to.x = to.x - distance;
        to.y = to.y + humanFingerChubbiness;
    }
    else if (direction == Direction.Right) {
        to.x = to.x + distance;
        to.y = to.y + humanFingerChubbiness;
    }

    if (!duration) {
        UIATarget.localTarget().flickFromTo(point, to);
    }
    else {
        UIATarget.localTarget().dragFromToForDuration(point, to, duration);
    }
    wait(2);
}

/// Find an element whose name contains str within element
function findChild(element, str, type) {
    var splitIdx = str.indexOf("::");
    if (splitIdx>=0) {
        var items = str.split("::");
        var first = findChild(element, items[0]);
        if (!first) return null;
        return findChild(first, str.substring(splitIdx+2));
    }

    var realtype = type;
    var realstr = str;

    var reg = /^\(\((.*)\)\)(.*)/;
    var rmatch = reg.exec(str);
    if (rmatch) {
        realtype = rmatch[1];
        realstr = rmatch[2];
    }

    var skipCount = 0;
    var reg2 = /(.*)\[\[(\d+)\]\]$/;
    var rmatch2 = reg2.exec(realstr);
    if (rmatch2) {
        realstr = rmatch2[1];
        skipCount = parseInt(rmatch2[2]);
    }

    var array = childrenArray(element);
    for (var i in array) {
        var element = array[i];
        if (!inScreen(element)) {
            continue;
        }
        var match = true;
        if (realstr.length) {
            if (!element.name()) {
                match = false;
            }
            else if (element.name().indexOf(realstr)<0) {
                match = false;
            }
        }
        if (realtype) {
            var info = " "+element;
            if (info.indexOf(realtype)<0) {
                match = false;
            }
        }
        if (match) {
            if (skipCount>0) {
                skipCount --;
            }
            else {
                return element;
            }
        }
    }
    return null;
}

/// Current time in seconds
function currentTimeInSec() {
    return (new Date().getTime() / 1000);
}

/// Allowing a maximum waiting time, and check whether the target element appears
/// Returns the element if it exists.
function waitForElement(target, maxTime) {
    var window = win();
    var child = findChild(window, target);
    var timeStarted = currentTimeInSec();
    var timeSpent = 0;
    while(!child && timeSpent<maxTime) {
        wait(1);
        child = findChild(window, target);
        timeSpent = currentTimeInSec() - timeStarted;
    }
    return child;
}

/// Perform task based on task array
function performTask(tasks) {
    if (!GlobalEnv.Pass) {
        return;
    }

    for (var i in tasks) {
        wait(1); // be tolerant at the beginning

        var step  = tasks[i];
        log("performing "+step);

        var missing = performStep(step);

        if (missing) {
            break;
        }
    }
    if (missing) {
        log("[error] cannot find "+ missing + " during step " + step);
        UIALogger.logFail("fail to find "+ missing + " during step " + step);
        // throw "fail";
        GlobalEnv.Pass = false;
    }
}

function performStep(step) {
    var window = win();
    var action = step[0];
    var missing = null;
    var rescueIdx = -1;

    if (Check == action) {
        waitForElement(step[1], DefaultMaxWaitingTime);
        for (var j in step) {
            if (j>0) {
                if (!findChild(window, step[j])) {
                    missing = step[j];
                    break;
                }
            }
        }

    }
    else if (Tap == action || TryTap == action) {
        waitForElement(step[1], DefaultMaxWaitingTime);
        var b = findChild(window, step[1]);
        if (!b) {
            missing = step[1];
        }
        else {
            try {
                // log("going to tap " + b + b.name() + rect2str(b.rect()) );
                if (TryTap ==action) {
                    UIATarget.localTarget().tap(centerPoint(b.rect()));
                }
                else if (b instanceof UIAButton) {
                    b.tap();
                }
                else {
                    UIATarget.localTarget().tap(centerPoint(b.rect()));
                }
            }
            catch(ex) {
                missing = step[1];
            }
        }
        if (action== Tap) {
            rescueIdx = 2;
        }
        else { // TryTap
            if (missing) {
                log("Failed to tap "+missing+" but it is fine");
            }
            missing = null; // it's fine
        }
    }
    else if (Investigate == action) {
        if (step.length>1) {
            var e = findChild(window, step[1]);
            if (!e) {
                missing = step[1];
            }
            else {
                logTree(e);
            }
        }
        else {
            logTree(window);
            // UIATarget.localTarget().logElementTree();
        }
    }
    else if (Input == action) {
        var kb = UIATarget.localTarget().frontMostApp().keyboard();
        if (kb) {
            kb.typeString(step[1]);
        }
        else {
            missing = "Keyboard";
            rescueIdx = 2;
        }
    }
    else if (Wait == action) {
        wait(step[1]);
    }
    else if (Scroll == action) {
        var point = screenCenter();
        if (step[2]) {
            var element = findChild(window, step[2]);
            if (element) {
                point = centerPoint(element.rect());
            }
        }
        swipe(point, step[1], null, 1);
    }
    else if (Swipe == action) {
        var point = screenCenter();
        if (step[2]) {
            var element = findChild(window, step[2]);
            if (element) {
                point = centerPoint(element.rect());
            }
        }
        swipe(point, step[1]);
    }
    else if (TapPoint == action) {
        UIATarget.localTarget().tap(step[1]);
    }
    else if (WaitFor == action) {
        var target = step[1];
        var maxTime = step[2];
        var child = waitForElement(target, maxTime);
        if (!child) {
            missing = target;
            rescueIdx = 3;
        }
    }
    else if (Pick == action) {
        waitForElement(step[1], DefaultMaxWaitingTime);
        var picker = findChild(window, step[1]);
        if (!picker || !(picker instanceof UIAPickerWheel) ) {
            missing = step[1];
        }
        else {
            try {
                picker.selectValue(step[2]);
            }
            catch(ex) {
                missing = step[2];
            }
        }
        rescueIdx = 3;
    }

    // see if we can rescue
    if (missing && rescueIdx && step[rescueIdx]) {
        rescueTask = step[rescueIdx];
        step.splice(rescueIdx,1);
        wait(1);
        log("rescuing with "+rescueTask);
        var rescueMissing = performStep(rescueTask);
        if (!rescueMissing) {
            wait(1);
            log("retrying "+step);
            missing = performStep(step);
        }
    }

    return missing;
}

<|endoftext|>


<commit_before>'use strict';

// btoa polyfill for IE<10 courtesy https://github.com/davidchambers/Base64.js

var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';

function E() {
  this.message = 'String contains an invalid character';
}
E.prototype = new Error;
E.prototype.code = 5;
E.prototype.name = 'InvalidCharacterError';

function btoa(input) {
  var str = String(input);
  var output = '';
  for (
    // initialize result and counter
    var block, charCode, idx = 0, map = chars;
    // if the next str index does not exist:
    //   change the mapping table to "="
    //   check if d has no fractional digits
    str.charAt(idx | 0) || (map = '=', idx % 1);
    // "8 - idx % 1 * 8" generates the sequence 2, 4, 6, 8
    output += map.charAt(63 & block >> 8 - idx % 1 * 8)
  ) {
    charCode = str.charCodeAt(idx += 3 / 4);
    if (charCode > 0xFF) {
      throw new E();
    }
    block = block << 8 | charCode;
  }
  return output;
}

module.exports = btoa;
<commit_msg>Delete btoa polyfill<commit_after><|endoftext|>


<commit_before>/* jslint node: true, esnext: true */

"use strict";

const route = require('koa-route'),
	step = require('kronos-step');

/**
 * Endpoint to link against koa route
 */
class RouteSendEndpoint extends step.endpoint.SendEndpoint {

	/**
	 * @param name {String} endpoint name
	 * @param owner {Step} the owner of the endpoint
	 * @param method {String} http method defaults to get
	 * @param content
	 */
	constructor(name, owner, path, method, content) {
		super(name, owner);

		// The path in the URL
		Object.defineProperty(this, 'path', {
			value: path
		});

		method = method ? method.toLowerCase() :  "get";

		// The HTTP method to use (GET, POST, ...)
		Object.defineProperty(this, 'method', {
			value: method
		});

		// TODO @Markus
		Object.defineProperty(this, 'content', {
			value: content
		});
	}

	get route() {
		return route[this.method](this.path, ctx => {
			this.owner.info(`${this.method} ${this.path}`);

			const request = ctx.request;
			const rout = {
				info: {
					request: request
				}
			};

			if (this.content) {
				rout.content = this.content;
			} else {
				rout.stream = ctx.req;
			}

			return this.send(rout).then(response => {
				this.owner.info(`${this.method} ${this.path}: ${JSON.stringify(response)}`);
				ctx.body = response;
			});
		});
	}
}

const httpRoutingStep = Object.assign({}, step.Step, {
	"name": "kronos-http-routing",
	"description": "routes http requests to endpoints",
	/*
	"services": {
		"listener": {
			"type": "koa"
		}
	},
	*/
	initialize(manager, scopeReporter, name, conf, props) {
		props.listener = {
			value: manager.serviceDeclare('koa', conf.listener || 'default-listener')
		};

		props._start = {
			value: function () {
				return this.listener.start().then(
					r => {
						for (let en in this.endpoints) {
							const route = this.endpoints[en].route;
							if (route)
								this.listener.koa.use(route);
						}
						return Promise.resolve(this);
					}
				);
			}
		};
	},

	createEndpoints(scopeReporter, conf) {
		for (let path in conf.endpoints) {
			const r = conf.endpoints[path];
			this.addEndpoint(new RouteSendEndpoint(r.name || path, this, path, r.method));
		}
	}
});

exports.registerWithManager = function (manager) {
	manager.registerStep(httpRoutingStep);
};
<commit_msg>fix: remove routes when stopping<commit_after>/* jslint node: true, esnext: true */

"use strict";

const route = require('koa-route'),
	step = require('kronos-step');

/**
 * Endpoint to link against a koa route
 */
class RouteSendEndpoint extends step.endpoint.SendEndpoint {

	/**
	 * @param name {String} endpoint name
	 * @param owner {Step} the owner of the endpoint
	 * @param method {String} http method defaults to get
	 * @param content
	 */
	constructor(name, owner, path, method, content) {
		super(name, owner);

		// The path in the URL
		Object.defineProperty(this, 'path', {
			value: path
		});

		method = method ? method.toLowerCase() :  "get";

		// The HTTP method to use (GET, POST, ...)
		Object.defineProperty(this, 'method', {
			value: method
		});

		// TODO planned for dynamic construction of a request
		Object.defineProperty(this, 'content', {
			value: content
		});
	}

	/**
	 *
	 */
	get route() {
		if (!this._route) {
			this._route = route[this.method](this.path, ctx => {
				this.owner.info(`${this.method} ${this.path}`);

				const request = ctx.request;
				const rout = {
					info: {
						request: request
					}
				};

				if (this.content) {
					// TODO
					rout.content = this.content;
				} else {
					rout.stream = ctx.req;
				}

				return this.send(rout).then(response => {
					this.owner.info(`${this.method} ${this.path}: ${JSON.stringify(response)}`);
					ctx.body = response;
				});
			});
		}
		return this._route;
	}

	toString() {
		return `${this.method} ${this.name}`;
	}
}

const httpRoutingStep = Object.assign({}, step.Step, {
	"name": "kronos-http-routing",
	"description": "routes http requests to endpoints",
	/*
	"services": {
		"listener": {
			"type": "koa"
		}
	},
	*/
	initialize(manager, scopeReporter, name, conf, props) {
		props.listener = {
			value: manager.serviceDeclare('koa', conf.listener || 'default-listener')
		};
	},

	_start() {
		return this.listener.start().then(
			r => {
				for (let en in this.endpoints) {
					const route = this.endpoints[en].route;
					if (route) {
						this.listener.koa.use(route);
					}
				}
				return Promise.resolve(this);
			}
		);
	},

	_stop() {
		for (let en in this.endpoints) {
			const route = this.endpoints[en].route;
			if (route) {
				this.trace(level => `removing route ${this.endpoints[en]}`);
				this.listener.koa.delete(route);
			}
		}
		return Promise.resolve(this);
	},

	createEndpoints(scopeReporter, conf) {
		for (let path in conf.endpoints) {
			const r = conf.endpoints[path];
			this.addEndpoint(new RouteSendEndpoint(r.name || path, this, path, r.method));
		}
	}
});

exports.registerWithManager = function (manager) {
	manager.registerStep(httpRoutingStep);
};
<|endoftext|>


<commit_before>'use babel';

/**
 * Copyright (C) 2016 Ivan Kravets. All rights reserved.
 *
 * This source file is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version 2
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

import * as config from '../config';
import * as utils from '../utils';
import {InitializeNewProjectView} from './view';
import child_process from 'child_process';
import ini from 'ini';
import path from 'path';
import promisify from 'promisify-node';

const fsp = promisify('fs');

const __TYPE_FILE = 'file';
const __TYPE_DIR = 'dir';
let __ALL_LIB_WATCHERS = new Map();
let __BUSY_REGISTRY = null;
let __INTENSIONS_CACHE = new Map();

export function cleanMiscCache() {
  __ALL_LIB_WATCHERS = null;
  __INTENSIONS_CACHE = null;
  __BUSY_REGISTRY = null;
}

export function setBusyRegistry(registry) {
  __BUSY_REGISTRY = registry;
}

export function command() {
  // Initialize view
  var view = new InitializeNewProjectView();
  var panel = atom.workspace.addModalPanel({item: view.getElement()});

  // Set buttons handlers
  view.handleCancel = () => panel.destroy();
  view.handleInit = () => {
    const projectPath = view.getDirectory();
    const selectedBoards = view.getSelectedBoards();
    return installPlatformsForBoards(selectedBoards, view)
      .then(() => view.setStatus('Performing initialization...'))
      .then(() => initializeProject(selectedBoards, projectPath))
      .then(() => {
        atom.notifications.addSuccess('PlatformIO: Project has been successfully initialized!', {
          detail: 'The next files/directories were created in "' + projectPath + '"\n' +
          '"platformio.ini" - Project Configuration File\n' +
          '"src" - Put your source code here\n' +
          '"lib" - Put here project specific (private) libraries'
        });
        utils.runAtomCommand('build:refresh-targets');
      }, (reason) => {
        const title = 'PlatformIO: Failed to initialize PlatformIO project!';
        atom.notifications.addError(title, {detail: reason, dismissable: true});
        console.error(title);
        console.error(reason);
      })
      .then(() => {
        if (-1 === atom.project.getPaths().indexOf(projectPath)) {
          atom.project.addPath(projectPath);
        }
        handleLibChanges([projectPath]);
      })
      .then(() => panel.destroy(), () => panel.destroy());
  };

  const paths = atom.project.getPaths();
  if (paths.length > 0) {
    view.addDirectories(paths, utils.getActiveProjectPath());
  }
}

export function initializeProject(boardIds, projectPath) {
  const boards = utils.getBoards();
  const args = ['init', '--ide', 'atom'];
  boardIds.forEach((boardId) => {
    args.push('--board');
    if ('id' in boards[boardId]) {
      args.push(boards[boardId].id);
    }
    else {
      args.push(boardId);
    }
  });
  args.push('--project-dir');
  args.push(projectPath);

  return utils.spawnPio(args);
}

function getPlatforms(boardIds) {
  const boards = utils.getBoards();
  const result = new Set();
  for (const boardId of boardIds) {
    result.add(boards[boardId].platform);
  }
  return result;
}

export function installPlatformsForBoards(boardIds, view) {
  let p = Promise.resolve();
  for (const platform of getPlatforms(boardIds)) {
    p = p.then(_setStatus(platform))
         .then(_installPlatform(platform));
  }
  return p;

  function _setStatus(platform) {
    return function() {
      return view.setStatus(`Installing platform: ${platform}`);
    };
  }

  function _installPlatform(platform) {
    return function() {
      return utils.spawnPio(['platforms', 'install', platform]);
    };
  }
}

export async function rebuildIndex(projectPath) {
  if (!projectPath) {
    atom.notifications.addError(
      'PlatformIO: Please open the project directory.');
    return;
  }

  try {
    const iniPath = path.join(projectPath, 'platformio.ini');
    if (!await utils.isFile(iniPath)) {
      atom.notifications.addError('PlatformIO: Please initialize new project first.');
      return;
    }

    const envNamePrefix = 'env:';
    const config = ini.parse((await fsp.readFile(iniPath)).toString());
    const configSections = Object.keys(config);
    let rebuildOccured = false;
    for (const section of configSections) {
      if (section.indexOf(envNamePrefix) === 0 && config[section].board) {
        startBusy();
        const args = ['init', '--ide', 'atom', '-b', config[section].board];
        try {
          await utils.spawnPio(args, {cwd: projectPath});
          atom.notifications.addSuccess(
            'PlatformIO: C/C++ Project Index (for Autocomplete, Linter) has been successfully rebuilt.'
          );
          rebuildOccured = true;
          stopBusy();
          break;
        } catch(e) {
          stopBusy();
          onFail();
        }
      }
    }

    if (!rebuildOccured) {
      atom.notifications.addWarning(
        'PlatformIO: Rebuild operation has been skipped (empty project).'
      );
    }
  } catch(e) {
    onFail();
  }

  function onFail() {
    atom.notifications.addWarning(
      'PlatformIO: Failed to rebuild C/C++ Project Index (for Autocomplete, Linter).'
    );
  }

  function startBusy () {
    __BUSY_REGISTRY && __BUSY_REGISTRY.begin(
      'platformio.index-rebuild-' + projectPath,
      `PlatformIO: Rebuilding C/C++ Project Index for ${projectPath}`);
  }

  function stopBusy () {
    __BUSY_REGISTRY && __BUSY_REGISTRY.end(
      'platformio.index-rebuild-' + projectPath);
  }
}

export function intendToPerformIndexRebuild(p, firstRun=true, recursionDepth=0) {
  if (!__INTENSIONS_CACHE.has(p)) {
    __INTENSIONS_CACHE.set(p, []);
  }
  const intensions = __INTENSIONS_CACHE.get(p);
  if (firstRun && intensions.length === 0) {
    atom.notifications.addInfo('PlatformIO: C/C++ Project Index will be rebuilt shortly.', {
      detail: `Libraries or configuration of project "${p}" have been changed. ` +
              'C/C++ Project Index (Autocomplete, Linter) will be rebuilt in order to make changes ' +
              'available in the PlatformIO IDE.',
    });
  }

  const now = Date.now();
  intensions.sort();
  if (intensions[intensions.length - 1] + config.AUTO_REBUILD_DELAY < now) {
    // No new intensions were made in last AUTO_REBUILD_DELAY ms
    intensions.splice(0, intensions.length);  // clear the array
    return rebuildIndex(p);
  } else if (firstRun) {
    intensions.push(now);
  }

  if (intensions.length > 0 && recursionDepth < 1000) {
    setTimeout(
      () => intendToPerformIndexRebuild(p, false, recursionDepth + 1),
      config.AUTO_REBUILD_DELAY
    );
  }
}

export async function ensureProjectsInited(projectPaths, force=false) {
  if (!atom.config.get('platformio-ide.autoRebuildAutocompleteIndex') && !force) {
    return;
  }

  const confFiles = ['.clang_complete', '.gcc-flags.json'];
  for (const projectPath of projectPaths) {
    try {
      const dirStat = await fsp.stat(projectPath);
      if (!dirStat || !dirStat.isDirectory()) {
        continue;
      }
      const projectFiles = await fsp.readdir(projectPath);
      if (projectFiles.indexOf('platformio.ini') === -1) {
        continue;
      }
      for (const file of confFiles) {
        if (projectFiles.indexOf(file) === -1) {
          intendToPerformIndexRebuild(projectPath);
          break;
        }
      }
    } catch(e) {
      console.warn(`An error occured while processing project under ${projectPath}: ` + e.toString());
      continue;
    }
  }
}

/**
 * Setup watches on library paths of given project paths.
 *
 * Each project has a set of watches:
 *  - on local `lib` directory;
 *  - on global `lib` directory;
 *  - on `platformio.ini`;
 *
 * When `platformio.ini` content changes, checks a global `lib` dir. If it has
 * been changed, a corresponging watch of on old dir should be disposed, and
 * a watch on new dir should be created instead.
 *
 * WIP!
 */
export function handleLibChanges(projectPaths) {
  let Directory, File;
  try {
    const pathwatcher = require(path.join(process.resourcesPath, 'app.asar', 'node_modules', 'pathwatcher'));
    Directory = pathwatcher.Directory;
    File = pathwatcher.File;
  } catch(e) {
    console.warn('Unable to import the pathwatcher module. ' +
                 'Automatic index rebuild on libraries changes will not be available.');
    return;
  }

  // Stop watching removed paths
  const currentPaths = atom.project.getPaths();
  const removedPaths = Array.from(__ALL_LIB_WATCHERS.keys()).filter(p => currentPaths.indexOf(p) === -1);
  clearLibChangeWatchers(removedPaths);

  // Update watches on open paths
  projectPaths.map(p => {
    if (!utils.isPioProject(p)) {
      return;
    }

    if (!__ALL_LIB_WATCHERS.has(p)) {
      __ALL_LIB_WATCHERS.set(p, []);
    }
    const existingWatches = __ALL_LIB_WATCHERS.get(p);

    const necessaryWatches = [
      {
        type: __TYPE_FILE,
        path: path.join(p, 'platformio.ini'),
      }
    ];

    const warningMessage = 'Failed to get library directories for watching';
    try {
      const args = ['-c', 'from os.path import join; from platformio import VERSION,util; print ":".join([join(util.get_home_dir(), "lib"), util.get_projectlib_dir(), util.get_projectlibdeps_dir()]) if VERSION[0] == 3 else util.get_lib_dir()'];
      const child = child_process.spawnSync(utils.getPythonExecutable(), args, {cwd: p});
      if (child.status === 0) {
        for (const libDir of child.stdout.toString().trim().split(':')) {
          necessaryWatches.push({
            type: __TYPE_DIR,
            path: libDir,
          });
        }
      } else {
        console.warn(warningMessage);
      }
    } catch(e) {
      console.warn(warningMessage);
    }

    // Dispose the watches that are not necessary anymore (e.g., when global
    // lib dir changes, the old watch has to be disposed before a new one is
    // created).
    const necessaryWathPaths = necessaryWatches.map(x => x.path);
    let i = existingWatches.length;
    while (i--) {
      // Iterating backwards in order to be able to delete the array elements
      // safely.
      const watch = existingWatches[i];
      if (!necessaryWathPaths.includes(watch.path)) {
        watch.disposable.dispose();
        existingWatches.splice(i, 1);
      }
    }

    const alreadyWatchedPaths = existingWatches.map(x => x.path);
    const watchesToAdd = necessaryWatches
      .filter(x => !alreadyWatchedPaths.includes(x.path));

    for (const watchConfig of watchesToAdd) {
      let pathwatcherInstance = null;
      switch (watchConfig.type) {

      case __TYPE_FILE:
        pathwatcherInstance = new File(watchConfig.path);
        watchConfig.disposable = pathwatcherInstance.onDidChange(() => {
          handleLibChanges([p]);
          intendToPerformIndexRebuild(p);
        });
        console.debug(`File watch added: ${watchConfig.path}`);
        break;

      case __TYPE_DIR:
        pathwatcherInstance = new Directory(watchConfig.path);
        setupLibDirWatch(watchConfig, pathwatcherInstance, p);
        console.debug(`Directory watch added: ${watchConfig.path}`);
        break;

      default:
        console.warn(
          `Incorrect watch type specified: '${watchConfig.type}'; ` +
          `whole config: ${JSON.stringify(watchConfig)}`
        );
        continue;
      }
      existingWatches.push(watchConfig);
    }
  });
}

function setupLibDirWatch(libObj, dir, projectPath) {
  if (!dir.existsSync()) {
    return;
  }

  let subdirectories = new Set();
  dir.getEntriesSync().forEach((entry) => {
    if (entry.isDirectory()) {
      subdirectories.add(entry.getPath());
    }
  });

  libObj.disposable = dir.onDidChange(() => {
    let libAdded = false;
    const currentSubdirectories = new Set();

    const entries = dir.getEntriesSync();
    entries.forEach((entry) => {
      if (entry.isDirectory()) {
        const p = entry.getPath();
        currentSubdirectories.add(p);
        if (!subdirectories.has(p)) {
          libAdded = true;
        }
      }
    });
    subdirectories = currentSubdirectories;

    if (libAdded) {
      intendToPerformIndexRebuild(projectPath);
    }
  });
}

export function clearLibChangeWatchers(paths) {
  paths = typeof paths === 'undefined' ? atom.project.getPaths() : paths;
  for (const p of paths) {
    if (__ALL_LIB_WATCHERS.has(p)) {
      __ALL_LIB_WATCHERS.get(p).map(function(item) {
        item.path = null;
        if (item.disposable && typeof item.disposable.dispose === 'function') {
          item.disposable.dispose();
        }
        item.disposable = null;
      });
      __ALL_LIB_WATCHERS.delete(p);
    }
  }
}
<commit_msg>Log rebuild error to console<commit_after>'use babel';

/**
 * Copyright (C) 2016 Ivan Kravets. All rights reserved.
 *
 * This source file is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version 2
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

import * as config from '../config';
import * as utils from '../utils';
import {InitializeNewProjectView} from './view';
import child_process from 'child_process';
import ini from 'ini';
import path from 'path';
import promisify from 'promisify-node';

const fsp = promisify('fs');

const __TYPE_FILE = 'file';
const __TYPE_DIR = 'dir';
let __ALL_LIB_WATCHERS = new Map();
let __BUSY_REGISTRY = null;
let __INTENSIONS_CACHE = new Map();

export function cleanMiscCache() {
  __ALL_LIB_WATCHERS = null;
  __INTENSIONS_CACHE = null;
  __BUSY_REGISTRY = null;
}

export function setBusyRegistry(registry) {
  __BUSY_REGISTRY = registry;
}

export function command() {
  // Initialize view
  var view = new InitializeNewProjectView();
  var panel = atom.workspace.addModalPanel({item: view.getElement()});

  // Set buttons handlers
  view.handleCancel = () => panel.destroy();
  view.handleInit = () => {
    const projectPath = view.getDirectory();
    const selectedBoards = view.getSelectedBoards();
    return installPlatformsForBoards(selectedBoards, view)
      .then(() => view.setStatus('Performing initialization...'))
      .then(() => initializeProject(selectedBoards, projectPath))
      .then(() => {
        atom.notifications.addSuccess('PlatformIO: Project has been successfully initialized!', {
          detail: 'The next files/directories were created in "' + projectPath + '"\n' +
          '"platformio.ini" - Project Configuration File\n' +
          '"src" - Put your source code here\n' +
          '"lib" - Put here project specific (private) libraries'
        });
        utils.runAtomCommand('build:refresh-targets');
      }, (reason) => {
        const title = 'PlatformIO: Failed to initialize PlatformIO project!';
        atom.notifications.addError(title, {detail: reason, dismissable: true});
        console.error(title);
        console.error(reason);
      })
      .then(() => {
        if (-1 === atom.project.getPaths().indexOf(projectPath)) {
          atom.project.addPath(projectPath);
        }
        handleLibChanges([projectPath]);
      })
      .then(() => panel.destroy(), () => panel.destroy());
  };

  const paths = atom.project.getPaths();
  if (paths.length > 0) {
    view.addDirectories(paths, utils.getActiveProjectPath());
  }
}

export function initializeProject(boardIds, projectPath) {
  const boards = utils.getBoards();
  const args = ['init', '--ide', 'atom'];
  boardIds.forEach((boardId) => {
    args.push('--board');
    if ('id' in boards[boardId]) {
      args.push(boards[boardId].id);
    }
    else {
      args.push(boardId);
    }
  });
  args.push('--project-dir');
  args.push(projectPath);

  return utils.spawnPio(args);
}

function getPlatforms(boardIds) {
  const boards = utils.getBoards();
  const result = new Set();
  for (const boardId of boardIds) {
    result.add(boards[boardId].platform);
  }
  return result;
}

export function installPlatformsForBoards(boardIds, view) {
  let p = Promise.resolve();
  for (const platform of getPlatforms(boardIds)) {
    p = p.then(_setStatus(platform))
         .then(_installPlatform(platform));
  }
  return p;

  function _setStatus(platform) {
    return function() {
      return view.setStatus(`Installing platform: ${platform}`);
    };
  }

  function _installPlatform(platform) {
    return function() {
      return utils.spawnPio(['platforms', 'install', platform]);
    };
  }
}

export async function rebuildIndex(projectPath) {
  if (!projectPath) {
    atom.notifications.addError(
      'PlatformIO: Please open the project directory.');
    return;
  }

  try {
    const iniPath = path.join(projectPath, 'platformio.ini');
    if (!await utils.isFile(iniPath)) {
      atom.notifications.addError('PlatformIO: Please initialize new project first.');
      return;
    }

    const envNamePrefix = 'env:';
    const config = ini.parse((await fsp.readFile(iniPath)).toString());
    const configSections = Object.keys(config);
    let rebuildOccured = false;
    for (const section of configSections) {
      if (section.indexOf(envNamePrefix) === 0 && config[section].board) {
        startBusy();
        const args = ['init', '--ide', 'atom', '-b', config[section].board];
        try {
          await utils.spawnPio(args, {cwd: projectPath});
          atom.notifications.addSuccess(
            'PlatformIO: C/C++ Project Index (for Autocomplete, Linter) has been successfully rebuilt.'
          );
          rebuildOccured = true;
          stopBusy();
          break;
        } catch(e) {
          console.error(e);
          stopBusy();
          onFail();
        }
      }
    }

    if (!rebuildOccured) {
      atom.notifications.addWarning(
        'PlatformIO: Rebuild operation has been skipped (empty project).'
      );
    }
  } catch(e) {
    onFail();
  }

  function onFail() {
    atom.notifications.addWarning(
      'PlatformIO: Failed to rebuild C/C++ Project Index (for Autocomplete, Linter).'
    );
  }

  function startBusy () {
    __BUSY_REGISTRY && __BUSY_REGISTRY.begin(
      'platformio.index-rebuild-' + projectPath,
      `PlatformIO: Rebuilding C/C++ Project Index for ${projectPath}`);
  }

  function stopBusy () {
    __BUSY_REGISTRY && __BUSY_REGISTRY.end(
      'platformio.index-rebuild-' + projectPath);
  }
}

export function intendToPerformIndexRebuild(p, firstRun=true, recursionDepth=0) {
  if (!__INTENSIONS_CACHE.has(p)) {
    __INTENSIONS_CACHE.set(p, []);
  }
  const intensions = __INTENSIONS_CACHE.get(p);
  if (firstRun && intensions.length === 0) {
    atom.notifications.addInfo('PlatformIO: C/C++ Project Index will be rebuilt shortly.', {
      detail: `Libraries or configuration of project "${p}" have been changed. ` +
              'C/C++ Project Index (Autocomplete, Linter) will be rebuilt in order to make changes ' +
              'available in the PlatformIO IDE.',
    });
  }

  const now = Date.now();
  intensions.sort();
  if (intensions[intensions.length - 1] + config.AUTO_REBUILD_DELAY < now) {
    // No new intensions were made in last AUTO_REBUILD_DELAY ms
    intensions.splice(0, intensions.length);  // clear the array
    return rebuildIndex(p);
  } else if (firstRun) {
    intensions.push(now);
  }

  if (intensions.length > 0 && recursionDepth < 1000) {
    setTimeout(
      () => intendToPerformIndexRebuild(p, false, recursionDepth + 1),
      config.AUTO_REBUILD_DELAY
    );
  }
}

export async function ensureProjectsInited(projectPaths, force=false) {
  if (!atom.config.get('platformio-ide.autoRebuildAutocompleteIndex') && !force) {
    return;
  }

  const confFiles = ['.clang_complete', '.gcc-flags.json'];
  for (const projectPath of projectPaths) {
    try {
      const dirStat = await fsp.stat(projectPath);
      if (!dirStat || !dirStat.isDirectory()) {
        continue;
      }
      const projectFiles = await fsp.readdir(projectPath);
      if (projectFiles.indexOf('platformio.ini') === -1) {
        continue;
      }
      for (const file of confFiles) {
        if (projectFiles.indexOf(file) === -1) {
          intendToPerformIndexRebuild(projectPath);
          break;
        }
      }
    } catch(e) {
      console.warn(`An error occured while processing project under ${projectPath}: ` + e.toString());
      continue;
    }
  }
}

/**
 * Setup watches on library paths of given project paths.
 *
 * Each project has a set of watches:
 *  - on local `lib` directory;
 *  - on global `lib` directory;
 *  - on `platformio.ini`;
 *
 * When `platformio.ini` content changes, checks a global `lib` dir. If it has
 * been changed, a corresponging watch of on old dir should be disposed, and
 * a watch on new dir should be created instead.
 *
 * WIP!
 */
export function handleLibChanges(projectPaths) {
  let Directory, File;
  try {
    const pathwatcher = require(path.join(process.resourcesPath, 'app.asar', 'node_modules', 'pathwatcher'));
    Directory = pathwatcher.Directory;
    File = pathwatcher.File;
  } catch(e) {
    console.warn('Unable to import the pathwatcher module. ' +
                 'Automatic index rebuild on libraries changes will not be available.');
    return;
  }

  // Stop watching removed paths
  const currentPaths = atom.project.getPaths();
  const removedPaths = Array.from(__ALL_LIB_WATCHERS.keys()).filter(p => currentPaths.indexOf(p) === -1);
  clearLibChangeWatchers(removedPaths);

  // Update watches on open paths
  projectPaths.map(p => {
    if (!utils.isPioProject(p)) {
      return;
    }

    if (!__ALL_LIB_WATCHERS.has(p)) {
      __ALL_LIB_WATCHERS.set(p, []);
    }
    const existingWatches = __ALL_LIB_WATCHERS.get(p);

    const necessaryWatches = [
      {
        type: __TYPE_FILE,
        path: path.join(p, 'platformio.ini'),
      }
    ];

    const warningMessage = 'Failed to get library directories for watching';
    try {
      const args = ['-c', 'from os.path import join; from platformio import VERSION,util; print ":".join([join(util.get_home_dir(), "lib"), util.get_projectlib_dir(), util.get_projectlibdeps_dir()]) if VERSION[0] == 3 else util.get_lib_dir()'];
      const child = child_process.spawnSync(utils.getPythonExecutable(), args, {cwd: p});
      if (child.status === 0) {
        for (const libDir of child.stdout.toString().trim().split(':')) {
          necessaryWatches.push({
            type: __TYPE_DIR,
            path: libDir,
          });
        }
      } else {
        console.warn(warningMessage);
      }
    } catch(e) {
      console.warn(warningMessage);
    }

    // Dispose the watches that are not necessary anymore (e.g., when global
    // lib dir changes, the old watch has to be disposed before a new one is
    // created).
    const necessaryWathPaths = necessaryWatches.map(x => x.path);
    let i = existingWatches.length;
    while (i--) {
      // Iterating backwards in order to be able to delete the array elements
      // safely.
      const watch = existingWatches[i];
      if (!necessaryWathPaths.includes(watch.path)) {
        watch.disposable.dispose();
        existingWatches.splice(i, 1);
      }
    }

    const alreadyWatchedPaths = existingWatches.map(x => x.path);
    const watchesToAdd = necessaryWatches
      .filter(x => !alreadyWatchedPaths.includes(x.path));

    for (const watchConfig of watchesToAdd) {
      let pathwatcherInstance = null;
      switch (watchConfig.type) {

      case __TYPE_FILE:
        pathwatcherInstance = new File(watchConfig.path);
        watchConfig.disposable = pathwatcherInstance.onDidChange(() => {
          handleLibChanges([p]);
          intendToPerformIndexRebuild(p);
        });
        console.debug(`File watch added: ${watchConfig.path}`);
        break;

      case __TYPE_DIR:
        pathwatcherInstance = new Directory(watchConfig.path);
        setupLibDirWatch(watchConfig, pathwatcherInstance, p);
        console.debug(`Directory watch added: ${watchConfig.path}`);
        break;

      default:
        console.warn(
          `Incorrect watch type specified: '${watchConfig.type}'; ` +
          `whole config: ${JSON.stringify(watchConfig)}`
        );
        continue;
      }
      existingWatches.push(watchConfig);
    }
  });
}

function setupLibDirWatch(libObj, dir, projectPath) {
  if (!dir.existsSync()) {
    return;
  }

  let subdirectories = new Set();
  dir.getEntriesSync().forEach((entry) => {
    if (entry.isDirectory()) {
      subdirectories.add(entry.getPath());
    }
  });

  libObj.disposable = dir.onDidChange(() => {
    let libAdded = false;
    const currentSubdirectories = new Set();

    const entries = dir.getEntriesSync();
    entries.forEach((entry) => {
      if (entry.isDirectory()) {
        const p = entry.getPath();
        currentSubdirectories.add(p);
        if (!subdirectories.has(p)) {
          libAdded = true;
        }
      }
    });
    subdirectories = currentSubdirectories;

    if (libAdded) {
      intendToPerformIndexRebuild(projectPath);
    }
  });
}

export function clearLibChangeWatchers(paths) {
  paths = typeof paths === 'undefined' ? atom.project.getPaths() : paths;
  for (const p of paths) {
    if (__ALL_LIB_WATCHERS.has(p)) {
      __ALL_LIB_WATCHERS.get(p).map(function(item) {
        item.path = null;
        if (item.disposable && typeof item.disposable.dispose === 'function') {
          item.disposable.dispose();
        }
        item.disposable = null;
      });
      __ALL_LIB_WATCHERS.delete(p);
    }
  }
}
<|endoftext|>