/* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ "use strict"; /** * DO NOT MODIFY THIS FILE DIRECTLY. * This file is generated from separate files stored in the GCLI project. * Please modify the files there and use the import script so the 2 projects * are kept in sync. * For more information, ask Joe Walker */ this.EXPORTED_SYMBOLS = [ "gcli" ]; var define = Components.utils.import("resource://gre/modules/devtools/Require.jsm", {}).define; var require = Components.utils.import("resource://gre/modules/devtools/Require.jsm", {}).require; var console = Components.utils.import("resource://gre/modules/devtools/Console.jsm", {}).console; var setTimeout = Components.utils.import("resource://gre/modules/Timer.jsm", {}).setTimeout; var clearTimeout = Components.utils.import("resource://gre/modules/Timer.jsm", {}).clearTimeout; var Node = Components.interfaces.nsIDOMNode; var HTMLElement = Components.interfaces.nsIDOMHTMLElement; var Event = Components.interfaces.nsIDOMEvent; /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/index', ['require', 'exports', 'module' , 'gcli/settings', 'gcli/api', 'gcli/types/selection', 'gcli/types/delegate', 'gcli/types/array', 'gcli/types/boolean', 'gcli/types/command', 'gcli/types/date', 'gcli/types/file', 'gcli/types/javascript', 'gcli/types/node', 'gcli/types/number', 'gcli/types/resource', 'gcli/types/setting', 'gcli/types/string', 'gcli/converters', 'gcli/converters/basic', 'gcli/converters/terminal', 'gcli/ui/intro', 'gcli/ui/focus', 'gcli/ui/fields/basic', 'gcli/ui/fields/javascript', 'gcli/ui/fields/selection', 'gcli/commands/connect', 'gcli/commands/context', 'gcli/commands/help', 'gcli/commands/pref', 'gcli/ui/ffdisplay'], function(require, exports, module) { 'use strict'; require('gcli/settings').startup(); var api = require('gcli/api'); api.populateApi(exports); exports.addItems(require('gcli/types/selection').items); exports.addItems(require('gcli/types/delegate').items); exports.addItems(require('gcli/types/array').items); exports.addItems(require('gcli/types/boolean').items); exports.addItems(require('gcli/types/command').items); exports.addItems(require('gcli/types/date').items); exports.addItems(require('gcli/types/file').items); exports.addItems(require('gcli/types/javascript').items); exports.addItems(require('gcli/types/node').items); exports.addItems(require('gcli/types/number').items); exports.addItems(require('gcli/types/resource').items); exports.addItems(require('gcli/types/setting').items); exports.addItems(require('gcli/types/string').items); exports.addItems(require('gcli/converters').items); exports.addItems(require('gcli/converters/basic').items); // Don't export the 'html' type to avoid use of innerHTML // exports.addItems(require('gcli/converters/html').items); exports.addItems(require('gcli/converters/terminal').items); exports.addItems(require('gcli/ui/intro').items); exports.addItems(require('gcli/ui/focus').items); exports.addItems(require('gcli/ui/fields/basic').items); exports.addItems(require('gcli/ui/fields/javascript').items); exports.addItems(require('gcli/ui/fields/selection').items); // Don't export the '{' command // exports.addItems(require('gcli/cli').items); exports.addItems(require('gcli/commands/connect').items); exports.addItems(require('gcli/commands/context').items); exports.addItems(require('gcli/commands/help').items); exports.addItems(require('gcli/commands/pref').items); /** * This code is internal and subject to change without notice. * createDisplay() for Firefox requires an options object with the following * members: * - contentDocument: From the window of the attached tab * - chromeDocument: GCLITerm.document * - environment.hudId: GCLITerm.hudId * - jsEnvironment.globalObject: 'window' * - jsEnvironment.evalFunction: 'eval' in a sandbox * - inputElement: GCLITerm.inputNode * - completeElement: GCLITerm.completeNode * - hintElement: GCLITerm.hintNode * - inputBackgroundElement: GCLITerm.inputStack */ exports.createDisplay = function(opts) { var FFDisplay = require('gcli/ui/ffdisplay').FFDisplay; return new FFDisplay(opts); }; var prefSvc = Components.classes['@mozilla.org/preferences-service;1'] .getService(Components.interfaces.nsIPrefService); var prefBranch = prefSvc.getBranch(null) .QueryInterface(Components.interfaces.nsIPrefBranch2); exports.hiddenByChromePref = function() { return !prefBranch.prefHasUserValue('devtools.chrome.enabled'); }; try { var Services = Components.utils.import('resource://gre/modules/Services.jsm', {}).Services; var stringBundle = Services.strings.createBundle( 'chrome://browser/locale/devtools/gclicommands.properties'); /** * Lookup a string in the GCLI string bundle */ exports.lookup = function(name) { try { return stringBundle.GetStringFromName(name); } catch (ex) { throw new Error('Failure in lookup(\'' + name + '\')'); } }; /** * Lookup a string in the GCLI string bundle */ exports.lookupFormat = function(name, swaps) { try { return stringBundle.formatStringFromName(name, swaps, swaps.length); } catch (ex) { throw new Error('Failure in lookupFormat(\'' + name + '\')'); } }; } catch (ex) { console.error('Using string fallbacks', ex); exports.lookup = function(name) { return name; }; exports.lookupFormat = function(name, swaps) { return name; }; } }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/settings', ['require', 'exports', 'module' , 'util/util', 'gcli/types'], function(require, exports, module) { 'use strict'; var imports = {}; var XPCOMUtils = Components.utils.import('resource://gre/modules/XPCOMUtils.jsm', {}).XPCOMUtils; var Services = Components.utils.import('resource://gre/modules/Services.jsm', {}).Services; XPCOMUtils.defineLazyGetter(imports, 'prefBranch', function() { var prefService = Components.classes['@mozilla.org/preferences-service;1'] .getService(Components.interfaces.nsIPrefService); return prefService.getBranch(null) .QueryInterface(Components.interfaces.nsIPrefBranch2); }); XPCOMUtils.defineLazyGetter(imports, 'supportsString', function() { return Components.classes['@mozilla.org/supports-string;1'] .createInstance(Components.interfaces.nsISupportsString); }); var util = require('util/util'); var types = require('gcli/types'); /** * All local settings have this prefix when used in Firefox */ var DEVTOOLS_PREFIX = 'devtools.gcli.'; /** * A class to wrap up the properties of a preference. * @see toolkit/components/viewconfig/content/config.js */ function Setting(prefSpec) { if (typeof prefSpec === 'string') { // We're coming from getAll() i.e. a full listing of prefs this.name = prefSpec; this.description = ''; } else { // A specific addition by GCLI this.name = DEVTOOLS_PREFIX + prefSpec.name; if (prefSpec.ignoreTypeDifference !== true && prefSpec.type) { if (this.type.name !== prefSpec.type) { throw new Error('Locally declared type (' + prefSpec.type + ') != ' + 'Mozilla declared type (' + this.type.name + ') for ' + this.name); } } this.description = prefSpec.description; } this.onChange = util.createEvent('Setting.onChange'); } /** * What type is this property: boolean/integer/string? */ Object.defineProperty(Setting.prototype, 'type', { get: function() { switch (imports.prefBranch.getPrefType(this.name)) { case imports.prefBranch.PREF_BOOL: return types.createType('boolean'); case imports.prefBranch.PREF_INT: return types.createType('number'); case imports.prefBranch.PREF_STRING: return types.createType('string'); default: throw new Error('Unknown type for ' + this.name); } }, enumerable: true }); /** * What type is this property: boolean/integer/string? */ Object.defineProperty(Setting.prototype, 'value', { get: function() { switch (imports.prefBranch.getPrefType(this.name)) { case imports.prefBranch.PREF_BOOL: return imports.prefBranch.getBoolPref(this.name); case imports.prefBranch.PREF_INT: return imports.prefBranch.getIntPref(this.name); case imports.prefBranch.PREF_STRING: var value = imports.prefBranch.getComplexValue(this.name, Components.interfaces.nsISupportsString).data; // In case of a localized string if (/^chrome:\/\/.+\/locale\/.+\.properties/.test(value)) { value = imports.prefBranch.getComplexValue(this.name, Components.interfaces.nsIPrefLocalizedString).data; } return value; default: throw new Error('Invalid value for ' + this.name); } }, set: function(value) { if (imports.prefBranch.prefIsLocked(this.name)) { throw new Error('Locked preference ' + this.name); } switch (imports.prefBranch.getPrefType(this.name)) { case imports.prefBranch.PREF_BOOL: imports.prefBranch.setBoolPref(this.name, value); break; case imports.prefBranch.PREF_INT: imports.prefBranch.setIntPref(this.name, value); break; case imports.prefBranch.PREF_STRING: imports.supportsString.data = value; imports.prefBranch.setComplexValue(this.name, Components.interfaces.nsISupportsString, imports.supportsString); break; default: throw new Error('Invalid value for ' + this.name); } Services.prefs.savePrefFile(null); }, enumerable: true }); /** * Reset this setting to it's initial default value */ Setting.prototype.setDefault = function() { imports.prefBranch.clearUserPref(this.name); Services.prefs.savePrefFile(null); }; /** * Collection of preferences for sorted access */ var settingsAll = []; /** * Collection of preferences for fast indexed access */ var settingsMap = new Map(); /** * Flag so we know if we've read the system preferences */ var hasReadSystem = false; /** * Clear out all preferences and return to initial state */ function reset() { settingsMap = new Map(); settingsAll = []; hasReadSystem = false; } /** * Reset everything on startup and shutdown because we're doing lazy loading */ exports.startup = function() { reset(); }; exports.shutdown = function() { reset(); }; /** * Load system prefs if they've not been loaded already * @return true */ function readSystem() { if (hasReadSystem) { return; } imports.prefBranch.getChildList('').forEach(function(name) { var setting = new Setting(name); settingsAll.push(setting); settingsMap.set(name, setting); }); settingsAll.sort(function(s1, s2) { return s1.name.localeCompare(s2.name); }); hasReadSystem = true; } /** * Get an array containing all known Settings filtered to match the given * filter (string) at any point in the name of the setting */ exports.getAll = function(filter) { readSystem(); if (filter == null) { return settingsAll; } return settingsAll.filter(function(setting) { return setting.name.indexOf(filter) !== -1; }); }; /** * Add a new setting. */ exports.addSetting = function(prefSpec) { var setting = new Setting(prefSpec); if (settingsMap.has(setting.name)) { // Once exists already, we're going to need to replace it in the array for (var i = 0; i < settingsAll.length; i++) { if (settingsAll[i].name === setting.name) { settingsAll[i] = setting; } } } settingsMap.set(setting.name, setting); exports.onChange({ added: setting.name }); return setting; }; /** * Getter for an existing setting. Generally use of this function should be * avoided. Systems that define a setting should export it if they wish it to * be available to the outside, or not otherwise. Use of this function breaks * that boundary and also hides dependencies. Acceptable uses include testing * and embedded uses of GCLI that pre-define all settings (e.g. Firefox) * @param name The name of the setting to fetch * @return The found Setting object, or undefined if the setting was not found */ exports.getSetting = function(name) { // We might be able to give the answer without needing to read all system // settings if this is an internal setting var found = settingsMap.get(name); if (!found) { found = settingsMap.get(DEVTOOLS_PREFIX + name); } if (found) { return found; } if (hasReadSystem) { return undefined; } else { readSystem(); found = settingsMap.get(name); if (!found) { found = settingsMap.get(DEVTOOLS_PREFIX + name); } return found; } }; /** * Event for use to detect when the list of settings changes */ exports.onChange = util.createEvent('Settings.onChange'); /** * Remove a setting. A no-op in this case */ exports.removeSetting = function() { }; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('util/util', ['require', 'exports', 'module' , 'util/promise'], function(require, exports, module) { 'use strict'; /* * A number of DOM manipulation and event handling utilities. */ //------------------------------------------------------------------------------ var eventDebug = false; /** * Patch up broken console API from node */ if (eventDebug) { if (console.group == null) { console.group = function() { console.log(arguments); }; } if (console.groupEnd == null) { console.groupEnd = function() { console.log(arguments); }; } } /** * Useful way to create a name for a handler, used in createEvent() */ function nameFunction(handler) { var scope = handler.scope ? handler.scope.constructor.name + '.' : ''; var name = handler.func.name; if (name) { return scope + name; } for (var prop in handler.scope) { if (handler.scope[prop] === handler.func) { return scope + prop; } } return scope + handler.func; } /** * Create an event. * For use as follows: * * function Hat() { * this.putOn = createEvent('Hat.putOn'); * ... * } * Hat.prototype.adorn = function(person) { * this.putOn({ hat: hat, person: person }); * ... * } * * var hat = new Hat(); * hat.putOn.add(function(ev) { * console.log('The hat ', ev.hat, ' has is worn by ', ev.person); * }, scope); * * @param name Optional name to help with debugging */ exports.createEvent = function(name) { var handlers = []; var fireHoldCount = 0; var heldEvents = []; var eventCombiner; /** * This is how the event is triggered. * @param ev The event object to be passed to the event listeners */ var event = function(ev) { if (fireHoldCount > 0) { heldEvents.push(ev); if (eventDebug) { console.log('Held fire: ' + name, ev); } return; } if (eventDebug) { console.group('Fire: ' + name + ' to ' + handlers.length + ' listeners', ev); } // Use for rather than forEach because it step debugs better, which is // important for debugging events for (var i = 0; i < handlers.length; i++) { var handler = handlers[i]; if (eventDebug) { console.log(nameFunction(handler)); } handler.func.call(handler.scope, ev); } if (eventDebug) { console.groupEnd(); } }; /** * Add a new handler function * @param func The function to call when this event is triggered * @param scope Optional 'this' object for the function call */ event.add = function(func, scope) { if (eventDebug) { console.log('Adding listener to ' + name); } handlers.push({ func: func, scope: scope }); }; /** * Remove a handler function added through add(). Both func and scope must * be strict equals (===) the values used in the call to add() * @param func The function to call when this event is triggered * @param scope Optional 'this' object for the function call */ event.remove = function(func, scope) { if (eventDebug) { console.log('Removing listener from ' + name); } var found = false; handlers = handlers.filter(function(test) { var match = (test.func === func && test.scope === scope); if (match) { found = true; } return !match; }); if (!found) { console.warn('Handler not found. Attached to ' + name); } }; /** * Remove all handlers. * Reset the state of this event back to it's post create state */ event.removeAll = function() { handlers = []; }; /** * Temporarily prevent this event from firing. * @see resumeFire(ev) */ event.holdFire = function() { if (eventDebug) { console.group('Holding fire: ' + name); } fireHoldCount++; }; /** * Resume firing events. * If there are heldEvents, then we fire one event to cover them all. If an * event combining function has been provided then we use that to combine the * events. Otherwise the last held event is used. * @see holdFire() */ event.resumeFire = function() { if (eventDebug) { console.groupEnd('Resume fire: ' + name); } if (fireHoldCount === 0) { throw new Error('fireHoldCount === 0 during resumeFire on ' + name); } fireHoldCount--; if (heldEvents.length === 0) { return; } if (heldEvents.length === 1) { event(heldEvents[0]); } else { var first = heldEvents[0]; var last = heldEvents[heldEvents.length - 1]; if (eventCombiner) { event(eventCombiner(first, last, heldEvents)); } else { event(last); } } heldEvents = []; }; /** * When resumeFire has a number of events to combine, by default it just * picks the last, however you can provide an eventCombiner which returns a * combined event. * eventCombiners will be passed 3 parameters: * - first The first event to be held * - last The last event to be held * - all An array containing all the held events * The return value from an eventCombiner is expected to be an event object */ Object.defineProperty(event, 'eventCombiner', { set: function(newEventCombiner) { if (typeof newEventCombiner !== 'function') { throw new Error('eventCombiner is not a function'); } eventCombiner = newEventCombiner; }, enumerable: true }); return event; }; //------------------------------------------------------------------------------ var promise = require('util/promise'); /** * Utility to convert a resolved promise to a concrete value. * Warning: This is something of an experiment. The alternative of mixing * concrete/promise return values could be better. */ exports.synchronize = function(p) { if (p == null || typeof p.then !== 'function') { return p; } var failure; var reply; var onDone = function(value) { failure = false; reply = value; }; var onError = function (value) { failure = true; reply = value; }; p.then(onDone, onError); if (failure === undefined) { throw new Error('non synchronizable promise'); } if (failure) { throw reply; } return reply; }; /** * promiseEach is roughly like Array.forEach except that the action is taken to * be something that completes asynchronously, returning a promise, so we wait * for the action to complete for each array element before moving onto the * next. * @param array An array of objects to enumerate * @param action A function to call for each member of the array * @param scope Optional object to use as 'this' for the function calls * @return A promise which is resolved (with an array of resolution values) * when all the array members have been passed to the action function, and * rejected as soon as any of the action function calls failsĀ  */ exports.promiseEach = function(array, action, scope) { if (array.length === 0) { return promise.resolve([]); } var deferred = promise.defer(); var replies = []; var callNext = function(index) { var onSuccess = function(reply) { replies[index] = reply; if (index + 1 >= array.length) { deferred.resolve(replies); } else { callNext(index + 1); } }; var onFailure = function(ex) { deferred.reject(ex); }; var reply = action.call(scope, array[index], index, array); promise.resolve(reply).then(onSuccess).then(null, onFailure); }; callNext(0); return deferred.promise; }; /** * Catching errors from promises isn't as simple as: * promise.then(handler, console.error); * for a number of reasons: * - chrome's console doesn't have bound functions (why?) * - we don't get stack traces out from console.error(ex); */ exports.errorHandler = function(ex) { if (ex instanceof Error) { // V8 weirdly includes the exception message in the stack if (ex.stack.indexOf(ex.message) !== -1) { console.error(ex.stack); } else { console.error('' + ex); console.error(ex.stack); } } else { console.error(ex); } }; //------------------------------------------------------------------------------ /** * XHTML namespace */ exports.NS_XHTML = 'http://www.w3.org/1999/xhtml'; /** * XUL namespace */ exports.NS_XUL = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'; /** * Create an HTML or XHTML element depending on whether the document is HTML * or XML based. Where HTML/XHTML elements are distinguished by whether they * are created using doc.createElementNS('http://www.w3.org/1999/xhtml', tag) * or doc.createElement(tag) * If you want to create a XUL element then you don't have a problem knowing * what namespace you want. * @param doc The document in which to create the element * @param tag The name of the tag to create * @returns The created element */ exports.createElement = function(doc, tag) { if (exports.isXmlDocument(doc)) { return doc.createElementNS(exports.NS_XHTML, tag); } else { return doc.createElement(tag); } }; /** * Remove all the child nodes from this node * @param elem The element that should have it's children removed */ exports.clearElement = function(elem) { while (elem.hasChildNodes()) { elem.removeChild(elem.firstChild); } }; var isAllWhitespace = /^\s*$/; /** * Iterate over the children of a node looking for TextNodes that have only * whitespace content and remove them. * This utility is helpful when you have a template which contains whitespace * so it looks nice, but where the whitespace interferes with the rendering of * the page * @param elem The element which should have blank whitespace trimmed * @param deep Should this node removal include child elements */ exports.removeWhitespace = function(elem, deep) { var i = 0; while (i < elem.childNodes.length) { var child = elem.childNodes.item(i); if (child.nodeType === 3 /*Node.TEXT_NODE*/ && isAllWhitespace.test(child.textContent)) { elem.removeChild(child); } else { if (deep && child.nodeType === 1 /*Node.ELEMENT_NODE*/) { exports.removeWhitespace(child, deep); } i++; } } }; /** * Create a style element in the document head, and add the given CSS text to * it. * @param cssText The CSS declarations to append * @param doc The document element to work from * @param id Optional id to assign to the created style tag. If the id already * exists on the document, we do not add the CSS again. */ exports.importCss = function(cssText, doc, id) { if (!cssText) { return undefined; } doc = doc || document; if (!id) { id = 'hash-' + hash(cssText); } var found = doc.getElementById(id); if (found) { if (found.tagName.toLowerCase() !== 'style') { console.error('Warning: importCss passed id=' + id + ', but that pre-exists (and isn\'t a style tag)'); } return found; } var style = exports.createElement(doc, 'style'); style.id = id; style.appendChild(doc.createTextNode(cssText)); var head = doc.getElementsByTagName('head')[0] || doc.documentElement; head.appendChild(style); return style; }; /** * Simple hash function which happens to match Java's |String.hashCode()| * Done like this because I we don't need crypto-security, but do need speed, * and I don't want to spend a long time working on it. * @see http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/ */ function hash(str) { var h = 0; if (str.length === 0) { return h; } for (var i = 0; i < str.length; i++) { var character = str.charCodeAt(i); h = ((h << 5) - h) + character; h = h & h; // Convert to 32bit integer } return h; } /** * Shortcut for clearElement/createTextNode/appendChild to make up for the lack * of standards around textContent/innerText */ exports.setTextContent = function(elem, text) { exports.clearElement(elem); var child = elem.ownerDocument.createTextNode(text); elem.appendChild(child); }; /** * There are problems with innerHTML on XML documents, so we need to do a dance * using document.createRange().createContextualFragment() when in XML mode */ exports.setContents = function(elem, contents) { if (typeof HTMLElement !== 'undefined' && contents instanceof HTMLElement) { exports.clearElement(elem); elem.appendChild(contents); return; } if ('innerHTML' in elem) { elem.innerHTML = contents; } else { try { var ns = elem.ownerDocument.documentElement.namespaceURI; if (!ns) { ns = exports.NS_XHTML; } exports.clearElement(elem); contents = '
' + contents + '
'; var range = elem.ownerDocument.createRange(); var child = range.createContextualFragment(contents).firstChild; while (child.hasChildNodes()) { elem.appendChild(child.firstChild); } } catch (ex) { console.error('Bad XHTML', ex); console.trace(); throw ex; } } }; /** * Utility to find elements with href attributes and add a target=_blank * attribute to make sure that opened links will open in a new window. */ exports.linksToNewTab = function(element) { var links = element.ownerDocument.querySelectorAll('*[href]'); for (var i = 0; i < links.length; i++) { links[i].setAttribute('target', '_blank'); } return element; }; /** * Load some HTML into the given document and return a DOM element. * This utility assumes that the html has a single root (other than whitespace) */ exports.toDom = function(document, html) { var div = exports.createElement(document, 'div'); exports.setContents(div, html); return div.children[0]; }; /** * How to detect if we're in an XML document. * In a Mozilla we check that document.xmlVersion = null, however in Chrome * we use document.contentType = undefined. * @param doc The document element to work from (defaulted to the global * 'document' if missing */ exports.isXmlDocument = function(doc) { doc = doc || document; // Best test for Firefox if (doc.contentType && doc.contentType != 'text/html') { return true; } // Best test for Chrome if (doc.xmlVersion != null) { return true; } return false; }; /** * Find the position of [element] in [nodeList]. * @returns an index of the match, or -1 if there is no match */ function positionInNodeList(element, nodeList) { for (var i = 0; i < nodeList.length; i++) { if (element === nodeList[i]) { return i; } } return -1; } /** * We'd really like to be able to do 'new NodeList()' */ exports.createEmptyNodeList = function(doc) { if (doc.createDocumentFragment) { return doc.createDocumentFragment().childNodes; } return doc.querySelectorAll('x>:root'); }; /** * Find a unique CSS selector for a given element * @returns a string such that ele.ownerDocument.querySelector(reply) === ele * and ele.ownerDocument.querySelectorAll(reply).length === 1 */ exports.findCssSelector = function(ele) { var document = ele.ownerDocument; if (ele.id && document.getElementById(ele.id) === ele) { return '#' + ele.id; } // Inherently unique by tag name var tagName = ele.tagName.toLowerCase(); if (tagName === 'html') { return 'html'; } if (tagName === 'head') { return 'head'; } if (tagName === 'body') { return 'body'; } if (ele.parentNode == null) { console.log('danger: ' + tagName); } // We might be able to find a unique class name var selector, index, matches; if (ele.classList.length > 0) { for (var i = 0; i < ele.classList.length; i++) { // Is this className unique by itself? selector = '.' + ele.classList.item(i); matches = document.querySelectorAll(selector); if (matches.length === 1) { return selector; } // Maybe it's unique with a tag name? selector = tagName + selector; matches = document.querySelectorAll(selector); if (matches.length === 1) { return selector; } // Maybe it's unique using a tag name and nth-child index = positionInNodeList(ele, ele.parentNode.children) + 1; selector = selector + ':nth-child(' + index + ')'; matches = document.querySelectorAll(selector); if (matches.length === 1) { return selector; } } } // So we can be unique w.r.t. our parent, and use recursion index = positionInNodeList(ele, ele.parentNode.children) + 1; selector = exports.findCssSelector(ele.parentNode) + ' > ' + tagName + ':nth-child(' + index + ')'; return selector; }; /** * Work out the path for images. */ exports.createUrlLookup = function(callingModule) { return function imageUrl(path) { try { return require('text!gcli/ui/' + path); } catch (ex) { // Under node/unamd callingModule is provided by node. This code isn't // the right answer but it's enough to pass all the unit tests and get // test coverage information, which is all we actually care about here. if (callingModule.filename) { return callingModule.filename + path; } var filename = callingModule.id.split('/').pop() + '.js'; if (callingModule.uri.substr(-filename.length) !== filename) { console.error('Can\'t work out path from module.uri/module.id'); return path; } if (callingModule.uri) { var end = callingModule.uri.length - filename.length - 1; return callingModule.uri.substr(0, end) + '/' + path; } return filename + '/' + path; } }; }; //------------------------------------------------------------------------------ /** * Keyboard handling is a mess. http://unixpapa.com/js/key.html * It would be good to use DOM L3 Keyboard events, * http://www.w3.org/TR/2010/WD-DOM-Level-3-Events-20100907/#events-keyboardevents * however only Webkit supports them, and there isn't a shim on Monernizr: * https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-browser-Polyfills * and when the code that uses this KeyEvent was written, nothing was clear, * so instead, we're using this unmodern shim: * http://stackoverflow.com/questions/5681146/chrome-10-keyevent-or-something-similar-to-firefoxs-keyevent * See BUG 664991: GCLI's keyboard handling should be updated to use DOM-L3 * https://bugzilla.mozilla.org/show_bug.cgi?id=664991 */ if (typeof 'KeyEvent' === 'undefined') { exports.KeyEvent = this.KeyEvent; } else { exports.KeyEvent = { DOM_VK_CANCEL: 3, DOM_VK_HELP: 6, DOM_VK_BACK_SPACE: 8, DOM_VK_TAB: 9, DOM_VK_CLEAR: 12, DOM_VK_RETURN: 13, DOM_VK_ENTER: 14, DOM_VK_SHIFT: 16, DOM_VK_CONTROL: 17, DOM_VK_ALT: 18, DOM_VK_PAUSE: 19, DOM_VK_CAPS_LOCK: 20, DOM_VK_ESCAPE: 27, DOM_VK_SPACE: 32, DOM_VK_PAGE_UP: 33, DOM_VK_PAGE_DOWN: 34, DOM_VK_END: 35, DOM_VK_HOME: 36, DOM_VK_LEFT: 37, DOM_VK_UP: 38, DOM_VK_RIGHT: 39, DOM_VK_DOWN: 40, DOM_VK_PRINTSCREEN: 44, DOM_VK_INSERT: 45, DOM_VK_DELETE: 46, DOM_VK_0: 48, DOM_VK_1: 49, DOM_VK_2: 50, DOM_VK_3: 51, DOM_VK_4: 52, DOM_VK_5: 53, DOM_VK_6: 54, DOM_VK_7: 55, DOM_VK_8: 56, DOM_VK_9: 57, DOM_VK_SEMICOLON: 59, DOM_VK_EQUALS: 61, DOM_VK_A: 65, DOM_VK_B: 66, DOM_VK_C: 67, DOM_VK_D: 68, DOM_VK_E: 69, DOM_VK_F: 70, DOM_VK_G: 71, DOM_VK_H: 72, DOM_VK_I: 73, DOM_VK_J: 74, DOM_VK_K: 75, DOM_VK_L: 76, DOM_VK_M: 77, DOM_VK_N: 78, DOM_VK_O: 79, DOM_VK_P: 80, DOM_VK_Q: 81, DOM_VK_R: 82, DOM_VK_S: 83, DOM_VK_T: 84, DOM_VK_U: 85, DOM_VK_V: 86, DOM_VK_W: 87, DOM_VK_X: 88, DOM_VK_Y: 89, DOM_VK_Z: 90, DOM_VK_CONTEXT_MENU: 93, DOM_VK_NUMPAD0: 96, DOM_VK_NUMPAD1: 97, DOM_VK_NUMPAD2: 98, DOM_VK_NUMPAD3: 99, DOM_VK_NUMPAD4: 100, DOM_VK_NUMPAD5: 101, DOM_VK_NUMPAD6: 102, DOM_VK_NUMPAD7: 103, DOM_VK_NUMPAD8: 104, DOM_VK_NUMPAD9: 105, DOM_VK_MULTIPLY: 106, DOM_VK_ADD: 107, DOM_VK_SEPARATOR: 108, DOM_VK_SUBTRACT: 109, DOM_VK_DECIMAL: 110, DOM_VK_DIVIDE: 111, DOM_VK_F1: 112, DOM_VK_F2: 113, DOM_VK_F3: 114, DOM_VK_F4: 115, DOM_VK_F5: 116, DOM_VK_F6: 117, DOM_VK_F7: 118, DOM_VK_F8: 119, DOM_VK_F9: 120, DOM_VK_F10: 121, DOM_VK_F11: 122, DOM_VK_F12: 123, DOM_VK_F13: 124, DOM_VK_F14: 125, DOM_VK_F15: 126, DOM_VK_F16: 127, DOM_VK_F17: 128, DOM_VK_F18: 129, DOM_VK_F19: 130, DOM_VK_F20: 131, DOM_VK_F21: 132, DOM_VK_F22: 133, DOM_VK_F23: 134, DOM_VK_F24: 135, DOM_VK_NUM_LOCK: 144, DOM_VK_SCROLL_LOCK: 145, DOM_VK_COMMA: 188, DOM_VK_PERIOD: 190, DOM_VK_SLASH: 191, DOM_VK_BACK_QUOTE: 192, DOM_VK_OPEN_BRACKET: 219, DOM_VK_BACK_SLASH: 220, DOM_VK_CLOSE_BRACKET: 221, DOM_VK_QUOTE: 222, DOM_VK_META: 224 }; } }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('util/promise', ['require', 'exports', 'module' ], function(require, exports, module) { 'use strict'; var imported = {}; Components.utils.import('resource://gre/modules/commonjs/sdk/core/promise.js', imported); exports.defer = imported.Promise.defer; exports.resolve = imported.Promise.resolve; exports.reject = imported.Promise.reject; exports.promised = imported.Promise.promised; exports.all = imported.Promise.all; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/types', ['require', 'exports', 'module' , 'util/util', 'util/promise', 'gcli/argument'], function(require, exports, module) { 'use strict'; var util = require('util/util'); var promise = require('util/promise'); var Argument = require('gcli/argument').Argument; var BlankArgument = require('gcli/argument').BlankArgument; /** * Some types can detect validity, that is to say they can distinguish between * valid and invalid values. * We might want to change these constants to be numbers for better performance */ var Status = { /** * The conversion process worked without any problem, and the value is * valid. There are a number of failure states, so the best way to check * for failure is (x !== Status.VALID) */ VALID: { toString: function() { return 'VALID'; }, valueOf: function() { return 0; } }, /** * A conversion process failed, however it was noted that the string * provided to 'parse()' could be VALID by the addition of more characters, * so the typing may not be actually incorrect yet, just unfinished. * @see Status.ERROR */ INCOMPLETE: { toString: function() { return 'INCOMPLETE'; }, valueOf: function() { return 1; } }, /** * The conversion process did not work, the value should be null and a * reason for failure should have been provided. In addition some * completion values may be available. * @see Status.INCOMPLETE */ ERROR: { toString: function() { return 'ERROR'; }, valueOf: function() { return 2; } }, /** * A combined status is the worser of the provided statuses. The statuses * can be provided either as a set of arguments or a single array */ combine: function() { var combined = Status.VALID; for (var i = 0; i < arguments.length; i++) { var status = arguments[i]; if (Array.isArray(status)) { status = Status.combine.apply(null, status); } if (status > combined) { combined = status; } } return combined; }, fromString: function(str) { switch (str) { case Status.VALID.toString(): return Status.VALID; case Status.INCOMPLETE.toString(): return Status.INCOMPLETE; case Status.ERROR.toString(): return Status.ERROR; default: throw new Error('\'' + str + '\' is not a status'); } } }; exports.Status = Status; /** * The type.parse() method converts an Argument into a value, Conversion is * a wrapper to that value. * Conversion is needed to collect a number of properties related to that * conversion in one place, i.e. to handle errors and provide traceability. * @param value The result of the conversion * @param arg The data from which the conversion was made * @param status See the Status values [VALID|INCOMPLETE|ERROR] defined above. * The default status is Status.VALID. * @param message If status=ERROR, there should be a message to describe the * error. A message is not needed unless for other statuses, but could be * present for any status including VALID (in the case where we want to note a * warning, for example). * See BUG 664676: GCLI conversion error messages should be localized * @param predictions If status=INCOMPLETE, there could be predictions as to * the options available to complete the input. * We generally expect there to be about 7 predictions (to match human list * comprehension ability) however it is valid to provide up to about 20, * or less. It is the job of the predictor to decide a smart cut-off. * For example if there are 4 very good matches and 4 very poor ones, * probably only the 4 very good matches should be presented. * The predictions are presented either as an array of prediction objects or as * a function which returns this array when called with no parameters. * Each prediction object has the following shape: * { * name: '...', // textual completion. i.e. what the cli uses * value: { ... }, // value behind the textual completion * incomplete: true // this completion is only partial (optional) * } * The 'incomplete' property could be used to denote a valid completion which * could have sub-values (e.g. for tree navigation). */ function Conversion(value, arg, status, message, predictions) { // The result of the conversion process. Will be null if status != VALID this.value = value; // Allow us to trace where this Conversion came from this.arg = arg; if (arg == null) { throw new Error('Missing arg'); } if (predictions != null) { var toCheck = typeof predictions === 'function' ? predictions() : predictions; if (typeof toCheck.then !== 'function') { throw new Error('predictions is not a promise'); } toCheck.then(function(value) { if (!Array.isArray(value)) { throw new Error('prediction resolves to non array'); } }, util.errorHandler); } this._status = status || Status.VALID; this.message = message; this.predictions = predictions; } /** * Ensure that all arguments that are part of this conversion know what they * are assigned to. * @param assignment The Assignment (param/conversion link) to inform the * argument about. */ Object.defineProperty(Conversion.prototype, 'assignment', { get: function() { return this.arg.assignment; }, set: function(assignment) { this.arg.assignment = assignment; }, enumerable: true }); /** * Work out if there is information provided in the contained argument. */ Conversion.prototype.isDataProvided = function() { return this.arg.type !== 'BlankArgument'; }; /** * 2 conversions are equal if and only if their args are equal (argEquals) and * their values are equal (valueEquals). * @param that The conversion object to compare against. */ Conversion.prototype.equals = function(that) { if (this === that) { return true; } if (that == null) { return false; } return this.valueEquals(that) && this.argEquals(that); }; /** * Check that the value in this conversion is strict equal to the value in the * provided conversion. * @param that The conversion to compare values with */ Conversion.prototype.valueEquals = function(that) { return that != null && this.value === that.value; }; /** * Check that the argument in this conversion is equal to the value in the * provided conversion as defined by the argument (i.e. arg.equals). * @param that The conversion to compare arguments with */ Conversion.prototype.argEquals = function(that) { return that == null ? false : this.arg.equals(that.arg); }; /** * Accessor for the status of this conversion */ Conversion.prototype.getStatus = function(arg) { return this._status; }; /** * Defined by the toString() value provided by the argument */ Conversion.prototype.toString = function() { return this.arg.toString(); }; /** * If status === INCOMPLETE, then we may be able to provide predictions as to * how the argument can be completed. * @return An array of items, or a promise of an array of items, where each * item is an object with the following properties: * - name (mandatory): Displayed to the user, and typed in. No whitespace * - description (optional): Short string for display in a tool-tip * - manual (optional): Longer description which details usage * - incomplete (optional): Indicates that the prediction if used should not * be considered necessarily sufficient, which typically will mean that the * UI should not append a space to the completion * - value (optional): If a value property is present, this will be used as the * value of the conversion, otherwise the item itself will be used. */ Conversion.prototype.getPredictions = function() { if (typeof this.predictions === 'function') { return this.predictions(); } return promise.resolve(this.predictions || []); }; /** * Return a promise of an index constrained by the available predictions. * i.e. (index % predicitons.length) */ Conversion.prototype.constrainPredictionIndex = function(index) { if (index == null) { return promise.resolve(); } return this.getPredictions().then(function(value) { if (value.length === 0) { return undefined; } index = index % value.length; if (index < 0) { index = value.length + index; } return index; }.bind(this)); }; /** * Constant to allow everyone to agree on the maximum number of predictions * that should be provided. We actually display 1 less than this number. */ Conversion.maxPredictions = 11; exports.Conversion = Conversion; /** * ArrayConversion is a special Conversion, needed because arrays are converted * member by member rather then as a whole, which means we can track the * conversion if individual array elements. So an ArrayConversion acts like a * normal Conversion (which is needed as Assignment requires a Conversion) but * it can also be devolved into a set of Conversions for each array member. */ function ArrayConversion(conversions, arg) { this.arg = arg; this.conversions = conversions; this.value = conversions.map(function(conversion) { return conversion.value; }, this); this._status = Status.combine(conversions.map(function(conversion) { return conversion.getStatus(); })); // This message is just for reporting errors like "not enough values" // rather that for problems with individual values. this.message = ''; // Predictions are generally provided by individual values this.predictions = []; } ArrayConversion.prototype = Object.create(Conversion.prototype); Object.defineProperty(ArrayConversion.prototype, 'assignment', { get: function() { return this._assignment; }, set: function(assignment) { this._assignment = assignment; this.conversions.forEach(function(conversion) { conversion.assignment = assignment; }, this); }, enumerable: true }); ArrayConversion.prototype.getStatus = function(arg) { if (arg && arg.conversion) { return arg.conversion.getStatus(); } return this._status; }; ArrayConversion.prototype.isDataProvided = function() { return this.conversions.length > 0; }; ArrayConversion.prototype.valueEquals = function(that) { if (that == null) { return false; } if (!(that instanceof ArrayConversion)) { throw new Error('Can\'t compare values with non ArrayConversion'); } if (this.value === that.value) { return true; } if (this.value.length !== that.value.length) { return false; } for (var i = 0; i < this.conversions.length; i++) { if (!this.conversions[i].valueEquals(that.conversions[i])) { return false; } } return true; }; ArrayConversion.prototype.toString = function() { return '[ ' + this.conversions.map(function(conversion) { return conversion.toString(); }, this).join(', ') + ' ]'; }; exports.ArrayConversion = ArrayConversion; /** * Most of our types are 'static' e.g. there is only one type of 'string', * however some types like 'selection' and 'delegate' are customizable. * The basic Type type isn't useful, but does provide documentation about what * types do. */ function Type() { } /** * Convert the given value to a string representation. * Where possible, there should be round-tripping between values and their * string representations. * @param value The object to convert into a string * @param context An ExecutionContext to allow basic Requisition access */ Type.prototype.stringify = function(value, context) { throw new Error('Not implemented'); }; /** * Convert the given arg to an instance of this type. * Where possible, there should be round-tripping between values and their * string representations. * @param arg An instance of Argument to convert. * @param context An ExecutionContext to allow basic Requisition access * @return Conversion */ Type.prototype.parse = function(arg, context) { throw new Error('Not implemented'); }; /** * A convenience method for times when you don't have an argument to parse * but instead have a string. * @see #parse(arg) */ Type.prototype.parseString = function(str, context) { return this.parse(new Argument(str), context); }; /** * The plug-in system, and other things need to know what this type is * called. The name alone is not enough to fully specify a type. Types like * 'selection' and 'delegate' need extra data, however this function returns * only the name, not the extra data. */ Type.prototype.name = undefined; /** * If there is some concept of a higher value, return it, * otherwise return undefined. */ Type.prototype.increment = function(value, context) { return undefined; }; /** * If there is some concept of a lower value, return it, * otherwise return undefined. */ Type.prototype.decrement = function(value, context) { return undefined; }; /** * The 'blank value' of most types is 'undefined', but there are exceptions; * This allows types to specify a better conversion from empty string than * 'undefined'. * 2 known examples of this are boolean -> false and array -> [] */ Type.prototype.getBlank = function(context) { return new Conversion(undefined, new BlankArgument(), Status.INCOMPLETE, ''); }; /** * This is something of a hack for the benefit of DelegateType which needs to * be able to lie about it's type for fields to accept it as one of their own. * Sub-types can ignore this unless they're DelegateType. * @param context An ExecutionContext to allow basic Requisition access */ Type.prototype.getType = function(context) { return this; }; /** * addItems allows registrations of a number of things. This allows it to know * what type of item, and how it should be registered. */ Type.prototype.item = 'type'; exports.Type = Type; /** * Private registry of types * Invariant: types[name] = type.name */ var registeredTypes = {}; exports.getTypeNames = function() { return Object.keys(registeredTypes); }; /** * Add a new type to the list available to the system. * You can pass 2 things to this function - either an instance of Type, in * which case we return this instance when #getType() is called with a 'name' * that matches type.name. * Also you can pass in a constructor (i.e. function) in which case when * #getType() is called with a 'name' that matches Type.prototype.name we will * pass the typeSpec into this constructor. */ exports.addType = function(type) { if (typeof type === 'object') { if (!type.name) { throw new Error('All registered types must have a name'); } if (type instanceof Type) { registeredTypes[type.name] = type; } else { var name = type.name; var parent = type.parent; type.name = parent; delete type.parent; registeredTypes[name] = exports.createType(type); type.name = name; type.parent = parent; } } else if (typeof type === 'function') { if (!type.prototype.name) { throw new Error('All registered types must have a name'); } registeredTypes[type.prototype.name] = type; } else { throw new Error('Unknown type: ' + type); } }; /** * Remove a type from the list available to the system */ exports.removeType = function(type) { delete registeredTypes[type.name]; }; /** * Find a type, previously registered using #addType() */ exports.createType = function(typeSpec) { if (typeof typeSpec === 'string') { typeSpec = { name: typeSpec }; } if (typeof typeSpec !== 'object') { throw new Error('Can\'t extract type from ' + typeSpec); } var type, newType; if (typeSpec.name == null || typeSpec.name == 'type') { type = Type; } else { type = registeredTypes[typeSpec.name]; } if (!type) { console.error('Known types: ' + Object.keys(registeredTypes).join(', ')); throw new Error('Unknown type: \'' + typeSpec.name + '\''); } if (typeof type === 'function') { newType = new type(typeSpec); } else { // clone 'type' newType = {}; copyProperties(type, newType); } // Copy the properties of typeSpec onto the new type copyProperties(typeSpec, newType); if (typeof type !== 'function') { if (typeof newType.constructor === 'function') { newType.constructor(); } } return newType; }; function copyProperties(src, dest) { for (var key in src) { var descriptor; var obj = src; while (true) { descriptor = Object.getOwnPropertyDescriptor(obj, key); if (descriptor != null) { break; } obj = Object.getPrototypeOf(obj); if (obj == null) { throw new Error('Can\'t find descriptor of ' + key); } } if ('value' in descriptor) { dest[key] = src[key]; } else if ('get' in descriptor) { Object.defineProperty(dest, key, { get: descriptor.get, set: descriptor.set, enumerable: descriptor.enumerable }); } else { throw new Error('Don\'t know how to copy ' + key + ' property.'); } } } }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/argument', ['require', 'exports', 'module' ], function(require, exports, module) { 'use strict'; /** * Thinking out loud here: * Arguments are an area where we could probably refactor things a bit better. * The split process in Requisition creates a set of Arguments, which are then * assigned. The assign process sometimes converts them into subtypes of * Argument. We might consider that what gets assigned is _always_ one of the * subtypes (or actually a different type hierarchy entirely) and that we * don't manipulate the prefix/text/suffix but just use the 'subtypes' as * filters which present a view of the underlying original Argument. */ /** * We record where in the input string an argument comes so we can report * errors against those string positions. * @param text The string (trimmed) that contains the argument * @param prefix Knowledge of quotation marks and whitespace used prior to the * text in the input string allows us to re-generate the original input from * the arguments. * @param suffix Any quotation marks and whitespace used after the text. * Whitespace is normally placed in the prefix to the succeeding argument, but * can be used here when this is the last argument. * @constructor */ function Argument(text, prefix, suffix) { if (text === undefined) { this.text = ''; this.prefix = ''; this.suffix = ''; } else { this.text = text; this.prefix = prefix !== undefined ? prefix : ''; this.suffix = suffix !== undefined ? suffix : ''; } } Argument.prototype.type = 'Argument'; /** * Return the result of merging these arguments. * case and some of the arguments are in quotation marks? */ Argument.prototype.merge = function(following) { // Is it possible that this gets called when we're merging arguments // for the single string? return new Argument( this.text + this.suffix + following.prefix + following.text, this.prefix, following.suffix); }; /** * Returns a new Argument like this one but with various items changed. * @param options Values to use in creating a new Argument. * Warning: some implementations of beget make additions to the options * argument. You should be aware of this in the unlikely event that you want to * reuse 'options' arguments. * Properties: * - text: The new text value * - prefixSpace: Should the prefix be altered to begin with a space? * - prefixPostSpace: Should the prefix be altered to end with a space? * - suffixSpace: Should the suffix be altered to end with a space? * - type: Constructor to use in creating new instances. Default: Argument * - dontQuote: Should we avoid adding prefix/suffix quotes when the text value * has a space? Needed when we're completing a sub-command. */ Argument.prototype.beget = function(options) { var text = this.text; var prefix = this.prefix; var suffix = this.suffix; if (options.text != null) { text = options.text; // We need to add quotes when the replacement string has spaces or is empty if (!options.dontQuote) { var needsQuote = text.indexOf(' ') >= 0 || text.length === 0; var hasQuote = /['"]$/.test(prefix); if (needsQuote && !hasQuote) { prefix = prefix + '\''; suffix = '\'' + suffix; } } } if (options.prefixSpace && prefix.charAt(0) !== ' ') { prefix = ' ' + prefix; } if (options.prefixPostSpace && prefix.charAt(prefix.length - 1) !== ' ') { prefix = prefix + ' '; } if (options.suffixSpace && suffix.charAt(suffix.length - 1) !== ' ') { suffix = suffix + ' '; } if (text === this.text && suffix === this.suffix && prefix === this.prefix) { return this; } var type = options.type || Argument; return new type(text, prefix, suffix); }; /** * We need to keep track of which assignment we've been assigned to */ Object.defineProperty(Argument.prototype, 'assignment', { get: function() { return this._assignment; }, set: function(assignment) { this._assignment = assignment; }, enumerable: true }); /** * Sub-classes of Argument are collections of arguments, getArgs() gets access * to the members of the collection in order to do things like re-create input * command lines. For the simple Argument case it's just an array containing * only this. */ Argument.prototype.getArgs = function() { return [ this ]; }; /** * We define equals to mean all arg properties are strict equals. * Used by Conversion.argEquals and Conversion.equals and ultimately * Assignment.equals to avoid reporting a change event when a new conversion * is assigned. */ Argument.prototype.equals = function(that) { if (this === that) { return true; } if (that == null || !(that instanceof Argument)) { return false; } return this.text === that.text && this.prefix === that.prefix && this.suffix === that.suffix; }; /** * Helper when we're putting arguments back together */ Argument.prototype.toString = function() { // BUG 664207: We should re-escape escaped characters // But can we do that reliably? return this.prefix + this.text + this.suffix; }; /** * Merge an array of arguments into a single argument. * All Arguments in the array are expected to have the same emitter */ Argument.merge = function(argArray, start, end) { start = (start === undefined) ? 0 : start; end = (end === undefined) ? argArray.length : end; var joined; for (var i = start; i < end; i++) { var arg = argArray[i]; if (!joined) { joined = arg; } else { joined = joined.merge(arg); } } return joined; }; /** * For test/debug use only. The output from this function is subject to wanton * random change without notice, and should not be relied upon to even exist * at some later date. */ Object.defineProperty(Argument.prototype, '_summaryJson', { get: function() { var assignStatus = this.assignment == null ? 'null' : this.assignment.param.name; return '<' + this.prefix + ':' + this.text + ':' + this.suffix + '>' + ' (a=' + assignStatus + ',' + ' t=' + this.type + ')'; }, enumerable: true }); exports.Argument = Argument; /** * BlankArgument is a marker that the argument wasn't typed but is there to * fill a slot. Assignments begin with their arg set to a BlankArgument. */ function BlankArgument() { this.text = ''; this.prefix = ''; this.suffix = ''; } BlankArgument.prototype = Object.create(Argument.prototype); BlankArgument.prototype.type = 'BlankArgument'; exports.BlankArgument = BlankArgument; /** * ScriptArgument is a marker that the argument is designed to be Javascript. * It also implements the special rules that spaces after the { or before the * } are part of the pre/suffix rather than the content, and that they are * never 'blank' so they can be used by Requisition._split() and not raise an * ERROR status due to being blank. */ function ScriptArgument(text, prefix, suffix) { this.text = text !== undefined ? text : ''; this.prefix = prefix !== undefined ? prefix : ''; this.suffix = suffix !== undefined ? suffix : ''; ScriptArgument._moveSpaces(this); } ScriptArgument.prototype = Object.create(Argument.prototype); ScriptArgument.prototype.type = 'ScriptArgument'; /** * Private/Dangerous: Alters a ScriptArgument to move the spaces at the start * or end of the 'text' into the prefix/suffix. With a string, " a " is 3 chars * long, but with a ScriptArgument, { a } is only one char long. * Arguments are generally supposed to be immutable, so this method should only * be called on a ScriptArgument that isn't exposed to the outside world yet. */ ScriptArgument._moveSpaces = function(arg) { while (arg.text.charAt(0) === ' ') { arg.prefix = arg.prefix + ' '; arg.text = arg.text.substring(1); } while (arg.text.charAt(arg.text.length - 1) === ' ') { arg.suffix = ' ' + arg.suffix; arg.text = arg.text.slice(0, -1); } }; /** * As Argument.beget that implements the space rule documented in the ctor. */ ScriptArgument.prototype.beget = function(options) { options.type = ScriptArgument; var begotten = Argument.prototype.beget.call(this, options); ScriptArgument._moveSpaces(begotten); return begotten; }; exports.ScriptArgument = ScriptArgument; /** * Commands like 'echo' with a single string argument, and used with the * special format like: 'echo a b c' effectively have a number of arguments * merged together. */ function MergedArgument(args, start, end) { if (!Array.isArray(args)) { throw new Error('args is not an array of Arguments'); } if (start === undefined) { this.args = args; } else { this.args = args.slice(start, end); } var arg = Argument.merge(this.args); this.text = arg.text; this.prefix = arg.prefix; this.suffix = arg.suffix; } MergedArgument.prototype = Object.create(Argument.prototype); MergedArgument.prototype.type = 'MergedArgument'; /** * Keep track of which assignment we've been assigned to, and allow the * original args to do the same. */ Object.defineProperty(MergedArgument.prototype, 'assignment', { get: function() { return this._assignment; }, set: function(assignment) { this._assignment = assignment; this.args.forEach(function(arg) { arg.assignment = assignment; }, this); }, enumerable: true }); MergedArgument.prototype.getArgs = function() { return this.args; }; MergedArgument.prototype.equals = function(that) { if (this === that) { return true; } if (that == null || !(that instanceof MergedArgument)) { return false; } // We might need to add a check that args is the same here return this.text === that.text && this.prefix === that.prefix && this.suffix === that.suffix; }; exports.MergedArgument = MergedArgument; /** * TrueNamedArguments are for when we have an argument like --verbose which * has a boolean value, and thus the opposite of '--verbose' is ''. */ function TrueNamedArgument(arg) { this.arg = arg; this.text = arg.text; this.prefix = arg.prefix; this.suffix = arg.suffix; } TrueNamedArgument.prototype = Object.create(Argument.prototype); TrueNamedArgument.prototype.type = 'TrueNamedArgument'; Object.defineProperty(TrueNamedArgument.prototype, 'assignment', { get: function() { return this._assignment; }, set: function(assignment) { this._assignment = assignment; if (this.arg) { this.arg.assignment = assignment; } }, enumerable: true }); TrueNamedArgument.prototype.getArgs = function() { return [ this.arg ]; }; TrueNamedArgument.prototype.equals = function(that) { if (this === that) { return true; } if (that == null || !(that instanceof TrueNamedArgument)) { return false; } return this.text === that.text && this.prefix === that.prefix && this.suffix === that.suffix; }; /** * As Argument.beget that rebuilds nameArg and valueArg */ TrueNamedArgument.prototype.beget = function(options) { if (options.text) { console.error('Can\'t change text of a TrueNamedArgument', this, options); } options.type = TrueNamedArgument; var begotten = Argument.prototype.beget.call(this, options); begotten.arg = new Argument(begotten.text, begotten.prefix, begotten.suffix); return begotten; }; exports.TrueNamedArgument = TrueNamedArgument; /** * FalseNamedArguments are for when we don't have an argument like --verbose * which has a boolean value, and thus the opposite of '' is '--verbose'. */ function FalseNamedArgument() { this.text = ''; this.prefix = ''; this.suffix = ''; } FalseNamedArgument.prototype = Object.create(Argument.prototype); FalseNamedArgument.prototype.type = 'FalseNamedArgument'; FalseNamedArgument.prototype.getArgs = function() { return [ ]; }; FalseNamedArgument.prototype.equals = function(that) { if (this === that) { return true; } if (that == null || !(that instanceof FalseNamedArgument)) { return false; } return this.text === that.text && this.prefix === that.prefix && this.suffix === that.suffix; }; exports.FalseNamedArgument = FalseNamedArgument; /** * A named argument is for cases where we have input in one of the following * formats: * * We model this as a normal argument but with a long prefix. * * There are 2 ways to construct a NamedArgument. One using 2 Arguments which * are taken to be the argument for the name (e.g. '--param') and one for the * value to assign to that parameter. * Alternatively, you can pass in the text/prefix/suffix values in the same * way as an Argument is constructed. If you do this then you are expected to * assign to nameArg and valueArg before exposing the new NamedArgument. */ function NamedArgument() { if (typeof arguments[0] === 'string') { this.nameArg = null; this.valueArg = null; this.text = arguments[0]; this.prefix = arguments[1]; this.suffix = arguments[2]; } else if (arguments[1] == null) { this.nameArg = arguments[0]; this.valueArg = null; this.text = ''; this.prefix = this.nameArg.toString(); this.suffix = ''; } else { this.nameArg = arguments[0]; this.valueArg = arguments[1]; this.text = this.valueArg.text; this.prefix = this.nameArg.toString() + this.valueArg.prefix; this.suffix = this.valueArg.suffix; } } NamedArgument.prototype = Object.create(Argument.prototype); NamedArgument.prototype.type = 'NamedArgument'; Object.defineProperty(NamedArgument.prototype, 'assignment', { get: function() { return this._assignment; }, set: function(assignment) { this._assignment = assignment; this.nameArg.assignment = assignment; if (this.valueArg != null) { this.valueArg.assignment = assignment; } }, enumerable: true }); NamedArgument.prototype.getArgs = function() { return this.valueArg ? [ this.nameArg, this.valueArg ] : [ this.nameArg ]; }; NamedArgument.prototype.equals = function(that) { if (this === that) { return true; } if (that == null) { return false; } if (!(that instanceof NamedArgument)) { return false; } // We might need to add a check that nameArg and valueArg are the same return this.text === that.text && this.prefix === that.prefix && this.suffix === that.suffix; }; /** * As Argument.beget that rebuilds nameArg and valueArg */ NamedArgument.prototype.beget = function(options) { options.type = NamedArgument; var begotten = Argument.prototype.beget.call(this, options); // Cut the prefix into |whitespace|non-whitespace|whitespace+quote so we can // rebuild nameArg and valueArg from the parts var matches = /^([\s]*)([^\s]*)([\s]*['"]?)$/.exec(begotten.prefix); if (this.valueArg == null && begotten.text === '') { begotten.nameArg = new Argument(matches[2], matches[1], matches[3]); begotten.valueArg = null; } else { begotten.nameArg = new Argument(matches[2], matches[1], ''); begotten.valueArg = new Argument(begotten.text, matches[3], begotten.suffix); } return begotten; }; exports.NamedArgument = NamedArgument; /** * An argument the groups together a number of plain arguments together so they * can be jointly assigned to a single array parameter */ function ArrayArgument() { this.args = []; } ArrayArgument.prototype = Object.create(Argument.prototype); ArrayArgument.prototype.type = 'ArrayArgument'; ArrayArgument.prototype.addArgument = function(arg) { this.args.push(arg); }; ArrayArgument.prototype.addArguments = function(args) { Array.prototype.push.apply(this.args, args); }; ArrayArgument.prototype.getArguments = function() { return this.args; }; Object.defineProperty(ArrayArgument.prototype, 'assignment', { get: function() { return this._assignment; }, set: function(assignment) { this._assignment = assignment; this.args.forEach(function(arg) { arg.assignment = assignment; }, this); }, enumerable: true }); ArrayArgument.prototype.getArgs = function() { return this.args; }; ArrayArgument.prototype.equals = function(that) { if (this === that) { return true; } if (that == null) { return false; } if (that.type !== 'ArrayArgument') { return false; } if (this.args.length !== that.args.length) { return false; } for (var i = 0; i < this.args.length; i++) { if (!this.args[i].equals(that.args[i])) { return false; } } return true; }; /** * Helper when we're putting arguments back together */ ArrayArgument.prototype.toString = function() { return '{' + this.args.map(function(arg) { return arg.toString(); }, this).join(',') + '}'; }; exports.ArrayArgument = ArrayArgument; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/api', ['require', 'exports', 'module' , 'gcli/canon', 'gcli/converters', 'gcli/types', 'gcli/settings', 'gcli/ui/fields'], function(require, exports, module) { 'use strict'; var canon = require('gcli/canon'); var converters = require('gcli/converters'); var types = require('gcli/types'); var settings = require('gcli/settings'); var fields = require('gcli/ui/fields'); /** * This is the heart of the API that we expose to the outside */ exports.getApi = function() { return { addCommand: canon.addCommand, removeCommand: canon.removeCommand, addConverter: converters.addConverter, removeConverter: converters.removeConverter, addType: types.addType, removeType: types.removeType, addItems: function(items) { items.forEach(function(item) { // Some items are registered using the constructor so we need to check // the prototype for the the type of the item var type = item.item; if (type == null && item.prototype) { type = item.prototype.item; } if (type === 'command') { canon.addCommand(item); } else if (type === 'type') { types.addType(item); } else if (type === 'converter') { converters.addConverter(item); } else if (type === 'setting') { settings.addSetting(item); } else if (type === 'field') { fields.addField(item); } else { console.error('Error for: ', item); throw new Error('item property not found'); } }); }, removeItems: function(items) { items.forEach(function(item) { if (item.item === 'command') { canon.removeCommand(item); } else if (item.item === 'type') { types.removeType(item); } else if (item.item === 'converter') { converters.removeConverter(item); } else if (item.item === 'settings') { settings.removeSetting(item); } else if (item.item === 'field') { fields.removeField(item); } else { throw new Error('item property not found'); } }); } }; }; /** * api.getApi() is clean, but generally we want to add the functions to the * 'exports' object. So this is a quick helper. */ exports.populateApi = function(obj) { var exportable = exports.getApi(); Object.keys(exportable).forEach(function(key) { obj[key] = exportable[key]; }); }; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/canon', ['require', 'exports', 'module' , 'util/util', 'util/l10n', 'gcli/types'], function(require, exports, module) { 'use strict'; var util = require('util/util'); var l10n = require('util/l10n'); var types = require('gcli/types'); var Status = require('gcli/types').Status; /** * Implement the localization algorithm for any documentation objects (i.e. * description and manual) in a command. * @param data The data assigned to a description or manual property * @param onUndefined If data == null, should we return the data untouched or * lookup a 'we don't know' key in it's place. */ function lookup(data, onUndefined) { if (data == null) { if (onUndefined) { return l10n.lookup(onUndefined); } return data; } if (typeof data === 'string') { return data; } if (typeof data === 'object') { if (data.key) { return l10n.lookup(data.key); } var locales = l10n.getPreferredLocales(); var translated; locales.some(function(locale) { translated = data[locale]; return translated != null; }); if (translated != null) { return translated; } console.error('Can\'t find locale in descriptions: ' + 'locales=' + JSON.stringify(locales) + ', ' + 'description=' + JSON.stringify(data)); return '(No description)'; } return l10n.lookup(onUndefined); } /** * The command object is mostly just setup around a commandSpec (as passed to * #addCommand()). */ function Command(commandSpec) { Object.keys(commandSpec).forEach(function(key) { this[key] = commandSpec[key]; }, this); if (!this.name) { throw new Error('All registered commands must have a name'); } if (this.params == null) { this.params = []; } if (!Array.isArray(this.params)) { throw new Error('command.params must be an array in ' + this.name); } this.hasNamedParameters = false; this.description = 'description' in this ? this.description : undefined; this.description = lookup(this.description, 'canonDescNone'); this.manual = 'manual' in this ? this.manual : undefined; this.manual = lookup(this.manual); // At this point this.params has nested param groups. We want to flatten it // out and replace the param object literals with Parameter objects var paramSpecs = this.params; this.params = []; this.paramGroups = {}; this._shortParams = {}; var addParam = function(param) { var groupName = param.groupName || Parameter.DEFAULT_GROUP_NAME; this.params.push(param); if (!this.paramGroups.hasOwnProperty(groupName)) { this.paramGroups[groupName] = []; } this.paramGroups[groupName].push(param); }.bind(this); // Track if the user is trying to mix default params and param groups. // All the non-grouped parameters must come before all the param groups // because non-grouped parameters can be assigned positionally, so their // index is important. We don't want 'holes' in the order caused by // parameter groups. var usingGroups = false; // In theory this could easily be made recursive, so param groups could // contain nested param groups. Current thinking is that the added // complexity for the UI probably isn't worth it, so this implementation // prevents nesting. paramSpecs.forEach(function(spec) { if (!spec.group) { var param = new Parameter(spec, this, null); addParam(param); if (!param.isPositionalAllowed) { this.hasNamedParameters = true; } if (usingGroups && param.groupName == null) { throw new Error('Parameters can\'t come after param groups.' + ' Ignoring ' + this.name + '/' + spec.name); } if (param.groupName != null) { usingGroups = true; } } else { spec.params.forEach(function(ispec) { var param = new Parameter(ispec, this, spec.group); addParam(param); if (!param.isPositionalAllowed) { this.hasNamedParameters = true; } }, this); usingGroups = true; } }, this); this.params.forEach(function(param) { if (param.short != null) { if (this._shortParams[param.short] != null) { throw new Error('Multiple params using short name ' + param.short); } this._shortParams[param.short] = param; } }, this); } /** * JSON serializer that avoids non-serializable data */ Object.defineProperty(Command.prototype, 'json', { get: function() { return { name: this.name, description: this.description, manual: this.manual, params: this.params.map(function(param) { return param.json; }), returnType: this.returnType, isParent: (this.exec == null) }; }, enumerable: true }); /** * Easy way to lookup parameters by short name */ Command.prototype.getParameterByShortName = function(short) { return this._shortParams[short]; }; exports.Command = Command; /** * A wrapper for a paramSpec so we can sort out shortened versions names for * option switches */ function Parameter(paramSpec, command, groupName) { this.command = command || { name: 'unnamed' }; this.paramSpec = paramSpec; this.name = this.paramSpec.name; this.type = this.paramSpec.type; this.short = this.paramSpec.short; if (this.short != null && !/[0-9A-Za-z]/.test(this.short)) { throw new Error('\'short\' value must be a single alphanumeric digit.'); } this.groupName = groupName; if (this.groupName != null) { if (this.paramSpec.option != null) { throw new Error('Can\'t have a "option" property in a nested parameter'); } } else { if (this.paramSpec.option != null) { this.groupName = this.paramSpec.option === true ? Parameter.DEFAULT_GROUP_NAME : '' + this.paramSpec.option; } } if (!this.name) { throw new Error('In ' + this.command.name + ': all params must have a name'); } var typeSpec = this.type; this.type = types.createType(typeSpec); if (this.type == null) { console.error('Known types: ' + types.getTypeNames().join(', ')); throw new Error('In ' + this.command.name + '/' + this.name + ': can\'t find type for: ' + JSON.stringify(typeSpec)); } // boolean parameters have an implicit defaultValue:false, which should // not be changed. See the docs. if (this.type.name === 'boolean' && this.paramSpec.defaultValue !== undefined) { throw new Error('In ' + this.command.name + '/' + this.name + ': boolean parameters can not have a defaultValue.' + ' Ignoring'); } // Check the defaultValue for validity. // Both undefined and null get a pass on this test. undefined is used when // there is no defaultValue, and null is used when the parameter is // optional, neither are required to parse and stringify. if (this._defaultValue != null) { try { // Passing null in for a context is bound to get us into trouble some day // in which case we'll need to mock one up in some way var context = null; var defaultText = this.type.stringify(this.paramSpec.defaultValue, context); var parsed = this.type.parseString(defaultText, context); parsed.then(function(defaultConversion) { if (defaultConversion.getStatus() !== Status.VALID) { console.error('In ' + this.command.name + '/' + this.name + ': Error round tripping defaultValue. status = ' + defaultConversion.getStatus()); } }.bind(this), util.errorHandler); } catch (ex) { throw new Error('In ' + this.command.name + '/' + this.name + ': ' + ex); } } // All parameters that can only be set via a named parameter must have a // non-undefined default value if (!this.isPositionalAllowed && this.paramSpec.defaultValue === undefined && this.type.getBlank == null && this.type.name !== 'boolean') { throw new Error('In ' + this.command.name + '/' + this.name + ': Missing defaultValue for optional parameter.'); } } /** * The default group name, when none is given explicitly */ Parameter.DEFAULT_GROUP_NAME = l10n.lookup('canonDefaultGroupName'); /** * type.getBlank can be expensive, so we delay execution where we can */ Object.defineProperty(Parameter.prototype, 'defaultValue', { get: function() { if (!('_defaultValue' in this)) { this._defaultValue = (this.paramSpec.defaultValue !== undefined) ? this.paramSpec.defaultValue : this.type.getBlank().value; } return this._defaultValue; }, enumerable : true }); /** * Does the given name uniquely identify this param (among the other params * in this command) * @param name The name to check */ Parameter.prototype.isKnownAs = function(name) { return (name === '--' + this.name) || (name === '-' + this.short); }; /** * Resolve the manual for this parameter, by looking in the paramSpec * and doing a l10n lookup */ Object.defineProperty(Parameter.prototype, 'manual', { get: function() { return lookup(this.paramSpec.manual || undefined); }, enumerable: true }); /** * Resolve the description for this parameter, by looking in the paramSpec * and doing a l10n lookup */ Object.defineProperty(Parameter.prototype, 'description', { get: function() { return lookup(this.paramSpec.description || undefined, 'canonDescNone'); }, enumerable: true }); /** * Is the user required to enter data for this parameter? (i.e. has * defaultValue been set to something other than undefined) */ Object.defineProperty(Parameter.prototype, 'isDataRequired', { get: function() { return this.defaultValue === undefined; }, enumerable: true }); /** * Reflect the paramSpec 'hidden' property (dynamically so it can change) */ Object.defineProperty(Parameter.prototype, 'hidden', { get: function() { return this.paramSpec.hidden; }, enumerable: true }); /** * Are we allowed to assign data to this parameter using positional * parameters? */ Object.defineProperty(Parameter.prototype, 'isPositionalAllowed', { get: function() { return this.groupName == null; }, enumerable: true }); /** * JSON serializer that avoids non-serializable data */ Object.defineProperty(Parameter.prototype, 'json', { get: function() { var json = { name: this.name, type: this.paramSpec.type, description: this.description }; if (this.defaultValue !== undefined && json.type !== 'boolean') { json.defaultValue = this.defaultValue; } if (this.option !== undefined) { json.option = this.option; } if (this.short !== undefined) { json.short = this.short; } return json; }, enumerable: true }); exports.Parameter = Parameter; /** * A canon is a store for a list of commands */ function Canon() { // A lookup hash of our registered commands this._commands = {}; // A sorted list of command names, we regularly want them in order, so pre-sort this._commandNames = []; // A lookup of the original commandSpecs by command name this._commandSpecs = {}; // Enable people to be notified of changes to the list of commands this.onCanonChange = util.createEvent('canon.onCanonChange'); } /** * Add a command to the canon of known commands. * This function is exposed to the outside world (via gcli/index). It is * documented in docs/index.md for all the world to see. * @param commandSpec The command and its metadata. * @return The new command */ Canon.prototype.addCommand = function(commandSpec) { if (this._commands[commandSpec.name] != null) { // Roughly canon.removeCommand() without the event call, which we do later delete this._commands[commandSpec.name]; this._commandNames = this._commandNames.filter(function(test) { return test !== commandSpec.name; }); } var command = new Command(commandSpec); this._commands[commandSpec.name] = command; this._commandNames.push(commandSpec.name); this._commandNames.sort(); this._commandSpecs[commandSpec.name] = commandSpec; this.onCanonChange(); return command; }; /** * Remove an individual command. The opposite of #addCommand(). * Removing a non-existent command is a no-op. * @param commandOrName Either a command name or the command itself. * @return true if a command was removed, false otherwise. */ Canon.prototype.removeCommand = function(commandOrName) { var name = typeof commandOrName === 'string' ? commandOrName : commandOrName.name; if (!this._commands[name]) { return false; } // See start of canon.addCommand if changing this code delete this._commands[name]; delete this._commandSpecs[name]; this._commandNames = this._commandNames.filter(function(test) { return test !== name; }); this.onCanonChange(); return true; }; /** * Retrieve a command by name * @param name The name of the command to retrieve */ Canon.prototype.getCommand = function(name) { // '|| undefined' is to silence 'reference to undefined property' warnings return this._commands[name] || undefined; }; /** * Get an array of all the registered commands. */ Canon.prototype.getCommands = function() { return Object.keys(this._commands).map(function(name) { return this._commands[name]; }, this); }; /** * Get an array containing the names of the registered commands. */ Canon.prototype.getCommandNames = function() { return this._commandNames.slice(0); }; /** * Get access to the stored commandMetaDatas (i.e. before they were made into * instances of Command/Parameters) so we can remote them. */ Canon.prototype.getCommandSpecs = function() { var specs = {}; Object.keys(this._commands).forEach(function(name) { var command = this._commands[name]; if (!command.noRemote) { specs[name] = command.json; } }.bind(this)); return specs; }; /** * Add a set of commands that are executed somewhere else. * @param prefix The name prefix that we assign to all command names * @param commandSpecs Presumably as obtained from getCommandSpecs on remote * @param remoter Function to call on exec of a new remote command. This is * defined just like an exec function (i.e. that takes args/context as params * and returns a promise) with one extra feature, that the context includes a * 'commandName' property that contains the original command name. * @param to URL-like string that describes where the commands are executed. * This is to complete the parent command description. */ Canon.prototype.addProxyCommands = function(prefix, commandSpecs, remoter, to) { var names = Object.keys(commandSpecs); if (this._commands[prefix] != null) { throw new Error(l10n.lookupFormat('canonProxyExists', [ prefix ])); } // We need to add the parent command so all the commands from the other // system have a parent this.addCommand({ name: prefix, isProxy: true, description: l10n.lookupFormat('canonProxyDesc', [ to ]), manual: l10n.lookupFormat('canonProxyManual', [ to ]) }); names.forEach(function(name) { var commandSpec = commandSpecs[name]; if (commandSpec.noRemote) { return; } if (!commandSpec.isParent) { commandSpec.exec = function(args, context) { context.commandName = name; return remoter(args, context); }.bind(this); } commandSpec.name = prefix + ' ' + commandSpec.name; commandSpec.isProxy = true; this.addCommand(commandSpec); }.bind(this)); }; /** * Add a set of commands that are executed somewhere else. * @param prefix The name prefix that we assign to all command names * @param commandSpecs Presumably as obtained from getCommandSpecs on remote * @param remoter Function to call on exec of a new remote command. This is * defined just like an exec function (i.e. that takes args/context as params * and returns a promise) with one extra feature, that the context includes a * 'commandName' property that contains the original command name. * @param to URL-like string that describes where the commands are executed. * This is to complete the parent command description. */ Canon.prototype.removeProxyCommands = function(prefix) { var toRemove = []; Object.keys(this._commandSpecs).forEach(function(name) { if (name.indexOf(prefix) === 0) { toRemove.push(name); } }.bind(this)); var removed = []; toRemove.forEach(function(name) { var command = this.getCommand(name); if (command.isProxy) { this.removeCommand(name); removed.push(name); } else { console.error('Skipping removal of \'' + name + '\' because it is not a proxy command.'); } }.bind(this)); return removed; }; var canon = new Canon(); exports.Canon = Canon; exports.addCommand = canon.addCommand.bind(canon); exports.removeCommand = canon.removeCommand.bind(canon); exports.onCanonChange = canon.onCanonChange; exports.getCommands = canon.getCommands.bind(canon); exports.getCommand = canon.getCommand.bind(canon); exports.getCommandNames = canon.getCommandNames.bind(canon); exports.getCommandSpecs = canon.getCommandSpecs.bind(canon); exports.addProxyCommands = canon.addProxyCommands.bind(canon); exports.removeProxyCommands = canon.removeProxyCommands.bind(canon); /** * CommandOutputManager stores the output objects generated by executed * commands. * * CommandOutputManager is exposed to the the outside world and could (but * shouldn't) be used before gcli.startup() has been called. * This could should be defensive to that where possible, and we should * certainly document if the use of it or similar will fail if used too soon. */ function CommandOutputManager() { this.onOutput = util.createEvent('CommandOutputManager.onOutput'); } exports.CommandOutputManager = CommandOutputManager; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('util/l10n', ['require', 'exports', 'module' ], function(require, exports, module) { 'use strict'; var XPCOMUtils = Components.utils.import('resource://gre/modules/XPCOMUtils.jsm', {}).XPCOMUtils; var Services = Components.utils.import('resource://gre/modules/Services.jsm', {}).Services; var imports = {}; XPCOMUtils.defineLazyGetter(imports, 'stringBundle', function () { return Services.strings.createBundle('chrome://browser/locale/devtools/gcli.properties'); }); /* * Not supported when embedded - we're doing things the Mozilla way not the * require.js way. */ exports.registerStringsSource = function(modulePath) { throw new Error('registerStringsSource is not available in mozilla'); }; exports.unregisterStringsSource = function(modulePath) { throw new Error('unregisterStringsSource is not available in mozilla'); }; exports.lookupSwap = function(key, swaps) { throw new Error('lookupSwap is not available in mozilla'); }; exports.lookupPlural = function(key, ord, swaps) { throw new Error('lookupPlural is not available in mozilla'); }; exports.getPreferredLocales = function() { return [ 'root' ]; }; /** @see lookup() in lib/util/l10n.js */ exports.lookup = function(key) { try { // Our memory leak hunter walks reachable objects trying to work out what // type of thing they are using object.constructor.name. If that causes // problems then we can avoid the unknown-key-exception with the following: /* if (key === 'constructor') { return { name: 'l10n-mem-leak-defeat' }; } */ return imports.stringBundle.GetStringFromName(key); } catch (ex) { console.error('Failed to lookup ', key, ex); return key; } }; /** @see propertyLookup in lib/util/l10n.js */ exports.propertyLookup = Proxy.create({ get: function(rcvr, name) { return exports.lookup(name); } }); /** @see lookupFormat in lib/util/l10n.js */ exports.lookupFormat = function(key, swaps) { try { return imports.stringBundle.formatStringFromName(key, swaps, swaps.length); } catch (ex) { console.error('Failed to format ', key, ex); return key; } }; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/converters', ['require', 'exports', 'module' , 'util/promise'], function(require, exports, module) { 'use strict'; var promise = require('util/promise'); // It's probably easiest to read this bottom to top /** * Best guess at creating a DOM element from random data */ var fallbackDomConverter = { from: '*', to: 'dom', exec: function(data, conversionContext) { return conversionContext.document.createTextNode(data || ''); } }; /** * Best guess at creating a string from random data */ var fallbackStringConverter = { from: '*', to: 'string', exec: function(data, conversionContext) { return data == null ? '' : data.toString(); } }; /** * Convert a view object to a DOM element */ var viewDomConverter = { item: 'converter', from: 'view', to: 'dom', exec: function(view, conversionContext) { return view.toDom(conversionContext.document); } }; /** * Convert a view object to a string */ var viewStringConverter = { item: 'converter', from: 'view', to: 'string', exec: function(view, conversionContext) { return view.toDom(conversionContext.document).textContent; } }; /** * Create a new converter by using 2 converters, one after the other */ function getChainConverter(first, second) { if (first.to !== second.from) { throw new Error('Chain convert impossible: ' + first.to + '!=' + second.from); } return { from: first.from, to: second.to, exec: function(data, conversionContext) { var intermediate = first.exec(data, conversionContext); return second.exec(intermediate, conversionContext); } }; } /** * This is where we cache the converters that we know about */ var converters = { from: {} }; /** * Add a new converter to the cache */ exports.addConverter = function(converter) { var fromMatch = converters.from[converter.from]; if (fromMatch == null) { fromMatch = {}; converters.from[converter.from] = fromMatch; } fromMatch[converter.to] = converter; }; /** * Remove an existing converter from the cache */ exports.removeConverter = function(converter) { var fromMatch = converters.from[converter.from]; if (fromMatch == null) { return; } if (fromMatch[converter.to] === converter) { fromMatch[converter.to] = null; } }; /** * Work out the best converter that we've got, for a given conversion. */ function getConverter(from, to) { var fromMatch = converters.from[from]; if (fromMatch == null) { return getFallbackConverter(from, to); } var converter = fromMatch[to]; if (converter == null) { // Someone is going to love writing a graph search algorithm to work out // the smallest number of conversions, or perhaps the least 'lossy' // conversion but for now the only 2 step conversion is foo->view->dom, // which we are going to special case. if (to === 'dom') { converter = fromMatch.view; if (converter != null) { return getChainConverter(converter, viewDomConverter); } } if (to === 'string') { converter = fromMatch.view; if (converter != null) { return getChainConverter(converter, viewStringConverter); } } return getFallbackConverter(from, to); } return converter; } /** * Helper for getConverter to pick the best fallback converter */ function getFallbackConverter(from, to) { console.error('No converter from ' + from + ' to ' + to + '. Using fallback'); if (to === 'dom') { return fallbackDomConverter; } if (to === 'string') { return fallbackStringConverter; } throw new Error('No conversion possible from ' + from + ' to ' + to + '.'); } /** * Convert some data from one type to another * @param data The object to convert * @param from The type of the data right now * @param to The type that we would like the data in * @param conversionContext An execution context (i.e. simplified requisition) which is * often required for access to a document, or createView function */ exports.convert = function(data, from, to, conversionContext) { if (from === to) { return promise.resolve(data); } return promise.resolve(getConverter(from, to).exec(data, conversionContext)); }; /** * Items for export */ exports.items = [ viewDomConverter, viewStringConverter ]; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/ui/fields', ['require', 'exports', 'module' , 'util/promise', 'util/util', 'gcli/argument'], function(require, exports, module) { 'use strict'; var promise = require('util/promise'); var util = require('util/util'); var KeyEvent = require('util/util').KeyEvent; var Argument = require('gcli/argument').Argument; /** * A Field is a way to get input for a single parameter. * This class is designed to be inherited from. It's important that all * subclasses have a similar constructor signature because they are created * via getField(...) * @param type The type to use in conversions * @param options A set of properties to help fields configure themselves: * - document: The document we use in calling createElement * - named: Is this parameter named? That is to say, are positional * arguments disallowed, if true, then we need to provide updates to * the command line that explicitly name the parameter in use * (e.g. --verbose, or --name Fred rather than just true or Fred) * - name: If this parameter is named, what name should we use * - requisition: The requisition that we're attached to * - required: Boolean to indicate if this is a mandatory field */ function Field(type, options) { this.type = type; this.document = options.document; this.requisition = options.requisition; } /** * Enable registration of fields using addItems */ Field.prototype.item = 'field'; /** * Subclasses should assign their element with the DOM node that gets added * to the 'form'. It doesn't have to be an input node, just something that * contains it. */ Field.prototype.element = undefined; /** * Indicates that this field should drop any resources that it has created */ Field.prototype.destroy = function() { delete this.messageElement; }; // Note: We could/should probably change Fields from working with Conversions // to working with Arguments (Tokens), which makes for less calls to parse() /** * Update this field display with the value from this conversion. * Subclasses should provide an implementation of this function. */ Field.prototype.setConversion = function(conversion) { throw new Error('Field should not be used directly'); }; /** * Extract a conversion from the values in this field. * Subclasses should provide an implementation of this function. */ Field.prototype.getConversion = function() { throw new Error('Field should not be used directly'); }; /** * Set the element where messages and validation errors will be displayed * @see setMessage() */ Field.prototype.setMessageElement = function(element) { this.messageElement = element; }; /** * Display a validation message in the UI */ Field.prototype.setMessage = function(message) { if (this.messageElement) { util.setTextContent(this.messageElement, message || ''); } }; /** * Method to be called by subclasses when their input changes, which allows us * to properly pass on the onFieldChange event. */ Field.prototype.onInputChange = function(ev) { promise.resolve(this.getConversion()).then(function(conversion) { this.onFieldChange({ conversion: conversion }); this.setMessage(conversion.message); if (ev.keyCode === KeyEvent.DOM_VK_RETURN) { this.requisition.exec(); } }.bind(this), util.errorHandler); }; /** * Some fields contain information that is more important to the user, for * example error messages and completion menus. */ Field.prototype.isImportant = false; /** * 'static/abstract' method to allow implementations of Field to lay a claim * to a type. This allows claims of various strength to be weighted up. * See the Field.*MATCH values. */ Field.claim = function(type, context) { throw new Error('Field should not be used directly'); }; /** * About minimalism - If we're producing a dialog, we want a field for every * parameter. If we're providing a quick tooltip, we only want a field when * it's really going to help. * The getField() function takes an option of 'tooltip: true'. Fields are * expected to reply with a TOOLTIP_* constant if they should be shown in the * tooltip case. */ Field.TOOLTIP_MATCH = 5; // A best match, that works for a tooltip Field.TOOLTIP_DEFAULT = 4; // A default match that should show in a tooltip Field.MATCH = 3; // Match, but ignorable if we're being minimalist Field.DEFAULT = 2; // This is a default (non-minimalist) match Field.BASIC = 1; // OK in an emergency. i.e. assume Strings Field.NO_MATCH = 0; // This field can't help with the given type exports.Field = Field; /** * Internal array of known fields */ var fieldCtors = []; /** * Add a field definition by field constructor * @param fieldCtor Constructor function of new Field */ exports.addField = function(fieldCtor) { if (typeof fieldCtor !== 'function') { console.error('addField erroring on ', fieldCtor); throw new Error('addField requires a Field constructor'); } fieldCtors.push(fieldCtor); }; /** * Remove a Field definition * @param field A previously registered field, specified either with a field * name or from the field name */ exports.removeField = function(field) { if (typeof field !== 'string') { fieldCtors = fieldCtors.filter(function(test) { return test !== field; }); } else if (field instanceof Field) { exports.removeField(field.name); } else { console.error('removeField erroring on ', field); throw new Error('removeField requires an instance of Field'); } }; /** * Find the best possible matching field from the specification of the type * of field required. * @param type An instance of Type that we will represent * @param options A set of properties that we should attempt to match, and use * in the construction of the new field object: * - document: The document to use in creating new elements * - name: The parameter name, (i.e. assignment.param.name) * - requisition: The requisition we're monitoring, * - required: Is this a required parameter (i.e. param.isDataRequired) * - named: Is this a named parameters (i.e. !param.isPositionalAllowed) * @return A newly constructed field that best matches the input options */ exports.getField = function(type, options) { var ctor; var highestClaim = -1; fieldCtors.forEach(function(fieldCtor) { var claim = fieldCtor.claim(type, options.requisition.executionContext); if (claim > highestClaim) { highestClaim = claim; ctor = fieldCtor; } }); if (!ctor) { console.error('Unknown field type ', type, ' in ', fieldCtors); throw new Error('Can\'t find field for ' + type); } if (options.tooltip && highestClaim < Field.TOOLTIP_DEFAULT) { return new BlankField(type, options); } return new ctor(type, options); }; /** * For use with delegate types that do not yet have anything to resolve to. * BlankFields are not for general use. */ function BlankField(type, options) { Field.call(this, type, options); this.element = util.createElement(this.document, 'div'); this.onFieldChange = util.createEvent('BlankField.onFieldChange'); } BlankField.prototype = Object.create(Field.prototype); BlankField.claim = function(type, context) { return type.name === 'blank' ? Field.MATCH : Field.NO_MATCH; }; BlankField.prototype.setConversion = function(conversion) { this.setMessage(conversion.message); }; BlankField.prototype.getConversion = function() { return this.type.parse(new Argument(), this.requisition.executionContext); }; exports.addField(BlankField); }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/types/selection', ['require', 'exports', 'module' , 'util/promise', 'util/util', 'util/l10n', 'util/spell', 'gcli/types', 'gcli/argument'], function(require, exports, module) { 'use strict'; var promise = require('util/promise'); var util = require('util/util'); var l10n = require('util/l10n'); var spell = require('util/spell'); var Type = require('gcli/types').Type; var Status = require('gcli/types').Status; var Conversion = require('gcli/types').Conversion; var BlankArgument = require('gcli/argument').BlankArgument; /** * A selection allows the user to pick a value from known set of options. * An option is made up of a name (which is what the user types) and a value * (which is passed to exec) * @param typeSpec Object containing properties that describe how this * selection functions. Properties include: * - lookup: An array of objects, one for each option, which contain name and * value properties. lookup can be a function which returns this array * - data: An array of strings - alternative to 'lookup' where the valid values * are strings. i.e. there is no mapping between what is typed and the value * that is used by the program * - stringifyProperty: Conversion from value to string is generally a process * of looking through all the valid options for a matching value, and using * the associated name. However the name maybe available directly from the * value using a property lookup. Setting 'stringifyProperty' allows * SelectionType to take this shortcut. * - cacheable: If lookup is a function, then we normally assume that * the values fetched can change. Setting 'cacheable:true' enables internal * caching. * - neverForceAsync: It's useful for testing purposes to be able to force all * selection types to be asynchronous. This flag prevents that happening for * types that are fundamentally synchronous. */ function SelectionType(typeSpec) { if (typeSpec) { Object.keys(typeSpec).forEach(function(key) { this[key] = typeSpec[key]; }, this); } } SelectionType.prototype = Object.create(Type.prototype); SelectionType.prototype.stringify = function(value, context) { if (value == null) { return ''; } if (this.stringifyProperty != null) { return value[this.stringifyProperty]; } try { var name = null; var lookup = util.synchronize(this.getLookup(context)); lookup.some(function(item) { if (item.value === value) { name = item.name; return true; } return false; }, this); return name; } catch (ex) { // Types really need to ensure stringify can happen synchronously // which means using stringifyProperty if getLookup is asynchronous, but // if this fails we need a bailout ... return value.toString(); } }; /** * If typeSpec contained cacheable:true then calls to parse() work on cached * data. clearCache() enables the cache to be cleared. */ SelectionType.prototype.clearCache = function() { delete this._cachedLookup; }; /** * There are several ways to get selection data. This unifies them into one * single function. * @return An array of objects with name and value properties. */ SelectionType.prototype.getLookup = function(context) { if (this._cachedLookup != null) { return this._cachedLookup; } var reply; if (this.lookup == null) { reply = resolve(this.data, context, this.neverForceAsync).then(dataToLookup); } else { var lookup = (typeof this.lookup === 'function') ? this.lookup.bind(this) : this.lookup; reply = resolve(lookup, context, this.neverForceAsync); } if (this.cacheable && !forceAsync) { this._cachedLookup = reply; } return reply; }; var forceAsync = false; /** * Both 'lookup' and 'data' properties (see docs on SelectionType constructor) * in addition to being real data can be a function or a promise, or even a * function which returns a promise of real data, etc. This takes a thing and * returns a promise of actual values. */ function resolve(thing, context, neverForceAsync) { if (forceAsync && !neverForceAsync) { var deferred = promise.defer(); setTimeout(function() { promise.resolve(thing).then(function(resolved) { if (typeof resolved === 'function') { resolved = resolve(resolved(), neverForceAsync); } deferred.resolve(resolved); }); }, 500); return deferred.promise; } return promise.resolve(thing).then(function(resolved) { if (typeof resolved === 'function') { return resolve(resolved(context), context, neverForceAsync); } return resolved; }); } /** * Selection can be provided with either a lookup object (in the 'lookup' * property) or an array of strings (in the 'data' property). Internally we * always use lookup, so we need a way to convert a 'data' array to a lookup. */ function dataToLookup(data) { if (!Array.isArray(data)) { throw new Error('SelectionType has no lookup or data'); } return data.map(function(option) { return { name: option, value: option }; }); } /** * Return a list of possible completions for the given arg. * @param arg The initial input to match * @return A trimmed array of string:value pairs */ SelectionType.prototype._findPredictions = function(arg, context) { return promise.resolve(this.getLookup(context)).then(function(lookup) { var predictions = []; var i, option; var maxPredictions = Conversion.maxPredictions; var match = arg.text.toLowerCase(); // If the arg has a suffix then we're kind of 'done'. Only an exact match // will do. if (arg.suffix.length > 0) { for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) { option = lookup[i]; if (option.name === arg.text) { predictions.push(option); } } return predictions; } // Cache lower case versions of all the option names for (i = 0; i < lookup.length; i++) { option = lookup[i]; if (option._gcliLowerName == null) { option._gcliLowerName = option.name.toLowerCase(); } } // Exact hidden matches. If 'hidden: true' then we only allow exact matches // All the tests after here check that !option.value.hidden for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) { option = lookup[i]; if (option.name === arg.text) { predictions.push(option); } } // Start with prefix matching for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) { option = lookup[i]; if (option._gcliLowerName.indexOf(match) === 0 && !option.value.hidden) { if (predictions.indexOf(option) === -1) { predictions.push(option); } } } // Try infix matching if we get less half max matched if (predictions.length < (maxPredictions / 2)) { for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) { option = lookup[i]; if (option._gcliLowerName.indexOf(match) !== -1 && !option.value.hidden) { if (predictions.indexOf(option) === -1) { predictions.push(option); } } } } // Try fuzzy matching if we don't get a prefix match if (predictions.length === 0) { var names = []; lookup.forEach(function(opt) { if (!opt.value.hidden) { names.push(opt.name); } }); var corrected = spell.correct(match, names); if (corrected) { lookup.forEach(function(opt) { if (opt.name === corrected) { predictions.push(opt); } }, this); } } return predictions; }.bind(this)); }; SelectionType.prototype.parse = function(arg, context) { return this._findPredictions(arg, context).then(function(predictions) { if (predictions.length === 0) { var msg = l10n.lookupFormat('typesSelectionNomatch', [ arg.text ]); return new Conversion(undefined, arg, Status.ERROR, msg, promise.resolve(predictions)); } if (predictions[0].name === arg.text) { var value = predictions[0].value; return new Conversion(value, arg, Status.VALID, '', promise.resolve(predictions)); } return new Conversion(undefined, arg, Status.INCOMPLETE, '', promise.resolve(predictions)); }.bind(this)); }; SelectionType.prototype.getBlank = function(context) { var predictFunc = function() { return promise.resolve(this.getLookup(context)).then(function(lookup) { return lookup.filter(function(option) { return !option.value.hidden; }).slice(0, Conversion.maxPredictions - 1); }); }.bind(this); return new Conversion(undefined, new BlankArgument(), Status.INCOMPLETE, '', predictFunc); }; /** * For selections, up is down and black is white. It's like this, given a list * [ a, b, c, d ], it's natural to think that it starts at the top and that * going up the list, moves towards 'a'. However 'a' has the lowest index, so * for SelectionType, up is down and down is up. * Sorry. */ SelectionType.prototype.decrement = function(value, context) { var lookup = util.synchronize(this.getLookup(context)); var index = this._findValue(lookup, value); if (index === -1) { index = 0; } index++; if (index >= lookup.length) { index = 0; } return lookup[index].value; }; /** * See note on SelectionType.decrement() */ SelectionType.prototype.increment = function(value, context) { var lookup = util.synchronize(this.getLookup(context)); var index = this._findValue(lookup, value); if (index === -1) { // For an increment operation when there is nothing to start from, we // want to start from the top, i.e. index 0, so the value before we // 'increment' (see note above) must be 1. index = 1; } index--; if (index < 0) { index = lookup.length - 1; } return lookup[index].value; }; /** * Walk through an array of { name:.., value:... } objects looking for a * matching value (using strict equality), returning the matched index (or -1 * if not found). * @param lookup Array of objects with name/value properties to search through * @param value The value to search for * @return The index at which the match was found, or -1 if no match was found */ SelectionType.prototype._findValue = function(lookup, value) { var index = -1; for (var i = 0; i < lookup.length; i++) { var pair = lookup[i]; if (pair.value === value) { index = i; break; } } return index; }; /** * SelectionType is designed to be inherited from, so SelectionField needs a way * to check if something works like a selection without using 'name' */ SelectionType.prototype.isSelection = true; SelectionType.prototype.name = 'selection'; exports.SelectionType = SelectionType; exports.items = [ SelectionType ]; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('util/spell', ['require', 'exports', 'module' ], function(require, exports, module) { 'use strict'; /* * A spell-checker based on Damerau-Levenshtein distance. */ var CASE_CHANGE_COST = 1; var INSERTION_COST = 10; var DELETION_COST = 10; var SWAP_COST = 10; var SUBSTITUTION_COST = 20; var MAX_EDIT_DISTANCE = 40; /** * Compute Damerau-Levenshtein Distance, with a modification to allow a low * case-change cost (1/10th of a swap-cost) * @see http://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance */ var distance = exports.distance = function(wordi, wordj) { var wordiLen = wordi.length; var wordjLen = wordj.length; // We only need to store three rows of our dynamic programming matrix. // (Without swap, it would have been two.) var row0 = new Array(wordiLen+1); var row1 = new Array(wordiLen+1); var row2 = new Array(wordiLen+1); var tmp; var i, j; // The distance between the empty string and a string of size i is the cost // of i insertions. for (i = 0; i <= wordiLen; i++) { row1[i] = i * INSERTION_COST; } // Row-by-row, we're computing the edit distance between substrings wordi[0..i] // and wordj[0..j]. for (j = 1; j <= wordjLen; j++) { // Edit distance between wordi[0..0] and wordj[0..j] is the cost of j // insertions. row0[0] = j * INSERTION_COST; for (i = 1; i <= wordiLen; i++) { // Handle deletion, insertion and substitution: we can reach each cell // from three other cells corresponding to those three operations. We // want the minimum cost. var dc = row0[i - 1] + DELETION_COST; var ic = row1[i] + INSERTION_COST; var sc0; if (wordi[i-1] === wordj[j-1]) { sc0 = 0; } else { if (wordi[i-1].toLowerCase() === wordj[j-1].toLowerCase()) { sc0 = CASE_CHANGE_COST; } else { sc0 = SUBSTITUTION_COST; } } var sc = row1[i-1] + sc0; row0[i] = Math.min(dc, ic, sc); // We handle swap too, eg. distance between help and hlep should be 1. If // we find such a swap, there's a chance to update row0[1] to be lower. if (i > 1 && j > 1 && wordi[i-1] === wordj[j-2] && wordj[j-1] === wordi[i-2]) { row0[i] = Math.min(row0[i], row2[i-2] + SWAP_COST); } } tmp = row2; row2 = row1; row1 = row0; row0 = tmp; } return row1[wordiLen]; }; /** * As distance() except that we say that if word is a prefix of name then we * only count the case changes. This allows us to use words that can be * completed by typing as more likely than short words */ var distancePrefix = exports.distancePrefix = function(word, name) { var dist = 0; for (var i = 0; i < word.length; i++) { if (name[i] !== word[i]) { if (name[i].toLowerCase() === word[i].toLowerCase()) { dist++; } else { // name does not start with word, even ignoring case, use // Damerau-Levenshtein return exports.distance(word, name); } } } return dist; }; /** * A function that returns the correction for the specified word. */ exports.correct = function(word, names) { if (names.length === 0) { return undefined; } var distances = {}; var sortedCandidates; names.forEach(function(candidate) { distances[candidate] = exports.distance(word, candidate); }); sortedCandidates = names.sort(function(worda, wordb) { if (distances[worda] !== distances[wordb]) { return distances[worda] - distances[wordb]; } else { // if the score is the same, always return the first string // in the lexicographical order return worda < wordb; } }); if (distances[sortedCandidates[0]] <= MAX_EDIT_DISTANCE) { return sortedCandidates[0]; } else { return undefined; } }; /** * Return a ranked list of matches: * * spell.rank('fred', [ 'banana', 'fred', 'ed', 'red' ]); * ā†“ * [ * { name: 'fred', dist: 0 }, * { name: 'red', dist: 1 }, * { name: 'ed', dist: 2 }, * { name: 'banana', dist: 10 }, * ] * * @param word The string that we're comparing names against * @param names An array of strings to compare word against * @param options Comparison options: * - noSort: Do not sort the output by distance * - prefixZero: Count prefix matches as edit distance 0 (i.e. word='bana' and * names=['banana'], would return { name:'banana': dist: 0 }) This is useful * if someone is typing the matches and may not have finished yet */ exports.rank = function(word, names, options) { options = options || {}; var reply = names.map(function(name) { // If any name starts with the word then the distance is based on the // number of case changes rather than Damerau-Levenshtein var algo = options.prefixZero ? distancePrefix : distance; return { name: name, dist: algo(word, name) }; }); if (!options.noSort) { reply = reply.sort(function(d1, d2) { return d1.dist - d2.dist; }); } return reply; }; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/types/delegate', ['require', 'exports', 'module' , 'util/promise', 'gcli/types'], function(require, exports, module) { 'use strict'; var promise = require('util/promise'); var Conversion = require('gcli/types').Conversion; /** * A type for "we don't know right now, but hope to soon" */ var delegate = { item: 'type', name: 'delegate', constructor: function() { if (typeof this.delegateType !== 'function') { throw new Error('Instances of DelegateType need typeSpec.delegateType' + ' to be a function that returns a type'); } }, // Child types should implement this method to return an instance of the type // that should be used. If no type is available, or some sort of temporary // placeholder is required, BlankType can be used. delegateType: function(context) { throw new Error('Not implemented'); }, stringify: function(value, context) { return this.delegateType(context).stringify(value, context); }, parse: function(arg, context) { return this.delegateType(context).parse(arg, context); }, decrement: function(value, context) { var delegated = this.delegateType(context); return (delegated.decrement ? delegated.decrement(value, context) : undefined); }, increment: function(value, context) { var delegated = this.delegateType(context); return (delegated.increment ? delegated.increment(value, context) : undefined); }, getType: function(context) { return this.delegateType(context); }, // DelegateType is designed to be inherited from, so DelegateField needs a way // to check if something works like a delegate without using 'name' isDelegate: true, }; Object.defineProperty(delegate, 'isImportant', { get: function() { return this.delegateType().isImportant; }, enumerable: true }); /** * 'blank' is a type for use with DelegateType when we don't know yet. * It should not be used anywhere else. */ var blank = { item: 'type', name: 'blank', stringify: function(value, context) { return ''; }, parse: function(arg, context) { return promise.resolve(new Conversion(undefined, arg)); } }; /** * The types we expose for registration */ exports.items = [ delegate, blank ]; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/types/array', ['require', 'exports', 'module' , 'util/promise', 'gcli/types', 'gcli/argument'], function(require, exports, module) { 'use strict'; var promise = require('util/promise'); var types = require('gcli/types'); var ArrayConversion = require('gcli/types').ArrayConversion; var ArrayArgument = require('gcli/argument').ArrayArgument; exports.items = [ { // A set of objects of the same type item: 'type', name: 'array', subtype: undefined, constructor: function() { if (!this.subtype) { console.error('Array.typeSpec is missing subtype. Assuming string.' + this.name); this.subtype = 'string'; } this.subtype = types.createType(this.subtype); }, stringify: function(values, context) { if (values == null) { return ''; } // BUG 664204: Check for strings with spaces and add quotes return values.join(' '); }, parse: function(arg, context) { if (arg.type !== 'ArrayArgument') { console.error('non ArrayArgument to ArrayType.parse', arg); throw new Error('non ArrayArgument to ArrayType.parse'); } // Parse an argument to a conversion // Hack alert. ArrayConversion needs to be able to answer questions about // the status of individual conversions in addition to the overall state. // |subArg.conversion| allows us to do that easily. var subArgParse = function(subArg) { return this.subtype.parse(subArg, context).then(function(conversion) { subArg.conversion = conversion; return conversion; }.bind(this)); }.bind(this); var conversionPromises = arg.getArguments().map(subArgParse); return promise.all(conversionPromises).then(function(conversions) { return new ArrayConversion(conversions, arg); }); }, getBlank: function() { return new ArrayConversion([], new ArrayArgument()); } }, ]; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/types/boolean', ['require', 'exports', 'module' , 'util/promise', 'gcli/types', 'gcli/types/selection', 'gcli/argument'], function(require, exports, module) { 'use strict'; var promise = require('util/promise'); var Status = require('gcli/types').Status; var Conversion = require('gcli/types').Conversion; var SelectionType = require('gcli/types/selection').SelectionType; var BlankArgument = require('gcli/argument').BlankArgument; exports.items = [ { // 'boolean' type item: 'type', name: 'boolean', parent: 'selection', lookup: [ { name: 'false', value: false }, { name: 'true', value: true } ], parse: function(arg, context) { if (arg.type === 'TrueNamedArgument') { return promise.resolve(new Conversion(true, arg)); } if (arg.type === 'FalseNamedArgument') { return promise.resolve(new Conversion(false, arg)); } return SelectionType.prototype.parse.call(this, arg, context); }, stringify: function(value, context) { if (value == null) { return ''; } return '' + value; }, getBlank: function(context) { return new Conversion(false, new BlankArgument(), Status.VALID, '', promise.resolve(this.lookup)); } } ]; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/types/command', ['require', 'exports', 'module' , 'util/promise', 'util/l10n', 'util/spell', 'gcli/canon', 'gcli/types/selection', 'gcli/types'], function(require, exports, module) { 'use strict'; var promise = require('util/promise'); var l10n = require('util/l10n'); var spell = require('util/spell'); var canon = require('gcli/canon'); var SelectionType = require('gcli/types/selection').SelectionType; var Status = require('gcli/types').Status; var Conversion = require('gcli/types').Conversion; exports.items = [ { // Select from the available parameters to a command item: 'type', name: 'param', parent: 'selection', stringifyProperty: 'name', neverForceAsync: true, requisition: undefined, isIncompleteName: undefined, lookup: function() { var displayedParams = []; var command = this.requisition.commandAssignment.value; if (command != null) { command.params.forEach(function(param) { var arg = this.requisition.getAssignment(param.name).arg; if (!param.isPositionalAllowed && arg.type === 'BlankArgument') { displayedParams.push({ name: '--' + param.name, value: param }); } }, this); } return displayedParams; }, parse: function(arg, context) { if (this.isIncompleteName) { return SelectionType.prototype.parse.call(this, arg, context); } else { var message = l10n.lookup('cliUnusedArg'); return promise.resolve(new Conversion(undefined, arg, Status.ERROR, message)); } } }, { // Select from the available commands // This is very similar to a SelectionType, however the level of hackery in // SelectionType to make it handle Commands correctly was to high, so we // simplified. // If you are making changes to this code, you should check there too. item: 'type', name: 'command', parent: 'selection', stringifyProperty: 'name', neverForceAsync: true, allowNonExec: true, lookup: function() { var commands = canon.getCommands(); commands.sort(function(c1, c2) { return c1.name.localeCompare(c2.name); }); return commands.map(function(command) { return { name: command.name, value: command }; }, this); }, parse: function(arg, context) { // Helper function - Commands like 'context' work best with parent // commands which are not executable. However obviously to execute a // command, it needs an exec function. var execWhereNeeded = function(command) { return this.allowNonExec || typeof command.exec === 'function'; }.bind(this); var command = canon.getCommand(arg.text); // Predictions live over the time that things change so we provide a // completion function rather than completion values var predictFunc = function() { return this._findPredictions(arg).then(function(predictions) { // If it's an exact match of an executable command (rather than just // the only possibility) then we don't want alternatives if (command && command.name === arg.text && execWhereNeeded(command) && predictions.length === 1) { return []; } return predictions; }.bind(this)); }.bind(this); if (command) { var status = execWhereNeeded(command) ? Status.VALID : Status.INCOMPLETE; var conversion = new Conversion(command, arg, status, '', predictFunc); return promise.resolve(conversion); } return this._findPredictions(arg).then(function(predictions) { if (predictions.length === 0) { var msg = l10n.lookupFormat('typesSelectionNomatch', [ arg.text ]); return new Conversion(undefined, arg, Status.ERROR, msg, predictFunc); } command = predictions[0].value; if (predictions.length === 1) { // Is it an exact match of an executable command, // or just the only possibility? if (command.name === arg.text && execWhereNeeded(command)) { return new Conversion(command, arg, Status.VALID, ''); } return new Conversion(undefined, arg, Status.INCOMPLETE, '', predictFunc); } // It's valid if the text matches, even if there are several options if (predictions[0].name === arg.text) { return new Conversion(command, arg, Status.VALID, '', predictFunc); } return new Conversion(undefined, arg, Status.INCOMPLETE, '', predictFunc); }.bind(this)); }, _findPredictions: function(arg, context) { return promise.resolve(this.getLookup(context)).then(function(lookup) { var predictions = []; var i, option; var maxPredictions = Conversion.maxPredictions; var match = arg.text.toLowerCase(); // Add an option to our list of predicted options var addToPredictions = function(option) { if (arg.text.length === 0) { // If someone hasn't typed anything, we only show top level commands in // the menu. i.e. sub-commands (those with a space in their name) are // excluded. We do this to keep the list at an overview level. if (option.name.indexOf(' ') === -1) { predictions.push(option); } } else { // If someone has typed something, then we exclude parent commands // (those without an exec). We do this because the user is drilling // down and doesn't need the summary level. if (option.value.exec != null) { predictions.push(option); } } }; // If the arg has a suffix then we're kind of 'done'. Only an exact // match will do. if (arg.suffix.match(/ +/)) { for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) { option = lookup[i]; if (option.name === arg.text || option.name.indexOf(arg.text + ' ') === 0) { addToPredictions(option); } } return predictions; } // Cache lower case versions of all the option names for (i = 0; i < lookup.length; i++) { option = lookup[i]; if (option._gcliLowerName == null) { option._gcliLowerName = option.name.toLowerCase(); } } // Exact hidden matches. If 'hidden: true' then we only allow exact matches // All the tests after here check that !option.value.hidden for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) { option = lookup[i]; if (option.name === arg.text) { addToPredictions(option); } } // Start with prefix matching for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) { option = lookup[i]; if (option._gcliLowerName.indexOf(match) === 0 && !option.value.hidden) { if (predictions.indexOf(option) === -1) { addToPredictions(option); } } } // Try infix matching if we get less half max matched if (predictions.length < (maxPredictions / 2)) { for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) { option = lookup[i]; if (option._gcliLowerName.indexOf(match) !== -1 && !option.value.hidden) { if (predictions.indexOf(option) === -1) { addToPredictions(option); } } } } // Try fuzzy matching if we don't get a prefix match if (predictions.length === 0) { var names = []; lookup.forEach(function(opt) { if (!opt.value.hidden) { names.push(opt.name); } }); var corrected = spell.correct(match, names); if (corrected) { lookup.forEach(function(opt) { if (opt.name === corrected) { predictions.push(opt); } }, this); } } return predictions; }.bind(this)); } } ]; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/types/date', ['require', 'exports', 'module' , 'util/promise', 'util/l10n', 'gcli/types'], function(require, exports, module) { 'use strict'; var promise = require('util/promise'); var l10n = require('util/l10n'); var Type = require('gcli/types').Type; var Status = require('gcli/types').Status; var Conversion = require('gcli/types').Conversion; function DateType(typeSpec) { // ECMA 5.1 Ā§15.9.1.1 // @see http://stackoverflow.com/questions/11526504/minimum-and-maximum-date typeSpec = typeSpec || {}; this._step = typeSpec.step || 1; this._min = new Date(-8640000000000000); this._max = new Date(8640000000000000); if (typeSpec.min != null) { if (typeof typeSpec.min === 'string') { this._min = toDate(typeSpec.min); } else if (isDate(typeSpec.min) || typeof typeSpec.min === 'function') { this._min = typeSpec.min; } else { throw new Error('date min value must be a string a date or a function'); } } if (typeSpec.max != null) { if (typeof typeSpec.max === 'string') { this._max = toDate(typeSpec.max); } else if (isDate(typeSpec.max) || typeof typeSpec.max === 'function') { this._max = typeSpec.max; } else { throw new Error('date max value must be a string a date or a function'); } } } DateType.prototype = Object.create(Type.prototype); /** * Helper for stringify() to left pad a single digit number with a single '0' * so 1 -> '01', 42 -> '42', etc. */ function pad(number) { var r = String(number); return r.length === 1 ? '0' + r : r; } DateType.prototype.stringify = function(value) { if (!isDate(value)) { return ''; } var str = pad(value.getFullYear()) + '-' + pad(value.getMonth() + 1) + '-' + pad(value.getDate()); // Only add in the time if it's not midnight if (value.getHours() !== 0 || value.getMinutes() !== 0 || value.getSeconds() !== 0 || value.getMilliseconds() !== 0) { // What string should we use to separate the date from the time? // There are 3 options: // 'T': This is the standard from ISO8601. i.e. 2013-05-20T11:05 // The good news - it's a standard. The bad news - it's weird and // alien to many if not most users // ' ': This looks nicest, but needs escaping (which GCLI will do // automatically) so it would look like: '2013-05-20 11:05' // Good news: looks best, bad news: on completion we place the cursor // after the final ', so repeated increment/decrement doesn't work // '\ ': It's possible that we could find a way to use a \ to escape the // space, so the output would look like: 2013-05-20\ 11:05 // This would involve changes to a number of parts, and is probably // too complex a solution for this problem for now // In the short term I'm going for ' ', and raising the priority of cursor // positioning on actions like increment/decrement/tab. str += ' ' + pad(value.getHours()); str += ':' + pad(value.getMinutes()); // Only add in seconds/milliseconds if there is anything to report if (value.getSeconds() !== 0 || value.getMilliseconds() !== 0) { str += ':' + pad(value.getSeconds()); if (value.getMilliseconds() !== 0) { str += '.' + String((value.getUTCMilliseconds()/1000).toFixed(3)).slice(2, 5); } } } return str; }; DateType.prototype.getMin = function(context) { if (typeof this._min === 'function') { return this._min(context); } if (isDate(this._min)) { return this._min; } return undefined; }; DateType.prototype.getMax = function(context) { if (typeof this._max === 'function') { return this._max(context); } if (isDate(this._max)) { return this._max; } return undefined; }; DateType.prototype.parse = function(arg, context) { var value; if (arg.text.replace(/\s/g, '').length === 0) { return promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE, '')); } // Lots of room for improvement here: 1h ago, in two days, etc. // Should "1h ago" dynamically update the step? if (arg.text.toLowerCase() === 'now' || arg.text.toLowerCase() === 'today') { value = new Date(); } else if (arg.text.toLowerCase() === 'yesterday') { value = new Date(); value.setDate(value.getDate() - 1); } else if (arg.text.toLowerCase() === 'tomorrow') { value = new Date(); value.setDate(value.getDate() + 1); } else { // So now actual date parsing. // Javascript dates are a mess. Like the default date libraries in most // common languages, but with added browser weirdness. // There is an argument for saying that the user will expect dates to be // formatted as JavaScript dates, except that JS dates are of themselves // very unexpected. // See http://blog.dygraphs.com/2012/03/javascript-and-dates-what-mess.html // The timezone used by Date.parse depends on whether or not the string // can be interpreted as ISO-8601, so "2000-01-01" is not the same as // "2000/01/01" (unless your TZ aligns with UTC) because the first is // ISO-8601 and therefore assumed to be UTC, where the latter is assumed to // be in the local timezone. // First, if the user explicitly includes a 'Z' timezone marker, then we // assume they know what they are doing with timezones. ISO-8601 uses 'Z' // as a marker for 'Zulu time', zero hours offset i.e. UTC if (arg.text.indexOf('Z') !== -1) { value = new Date(arg.text); } else { // Now we don't want the browser to assume ISO-8601 and therefore use UTC // so we replace the '-' with '/' value = new Date(arg.text.replace(/-/g, '/')); } if (isNaN(value.getTime())) { var msg = l10n.lookupFormat('typesDateNan', [ arg.text ]); return promise.resolve(new Conversion(undefined, arg, Status.ERROR, msg)); } } return promise.resolve(new Conversion(value, arg)); }; DateType.prototype.decrement = function(value, context) { if (!isDate(value)) { return new Date(); } var newValue = new Date(value); newValue.setDate(value.getDate() - this._step); if (newValue >= this.getMin(context)) { return newValue; } else { return this.getMin(context); } }; DateType.prototype.increment = function(value, context) { if (!isDate(value)) { return new Date(); } var newValue = new Date(value); newValue.setDate(value.getDate() + this._step); if (newValue <= this.getMax(context)) { return newValue; } else { return this.getMax(); } }; DateType.prototype.name = 'date'; exports.items = [ DateType ]; /** * Utility to convert a string to a date, throwing if the date can't be * parsed rather than having an invalid date */ function toDate(str) { var millis = Date.parse(str); if (isNaN(millis)) { throw new Error(l10n.lookupFormat('typesDateNan', [ str ])); } return new Date(millis); } /** * Is |thing| a valid date? * @see http://stackoverflow.com/questions/1353684/detecting-an-invalid-date-date-instance-in-javascript */ function isDate(thing) { return Object.prototype.toString.call(thing) === '[object Date]' && !isNaN(thing.getTime()); } }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/types/file', ['require', 'exports', 'module' , 'gcli/types/fileparser', 'gcli/types'], function(require, exports, module) { 'use strict'; /* * The file type is a bit of a spiders-web, but there isn't a nice solution * yet. The core of the problem is that the modules used by Firefox and NodeJS * intersect with the modules used by the web, but not each other. Except here. * So we have to do something fancy to get the sharing but not mess up the web. * * This file requires 'gcli/types/fileparser', and there are 4 implementations * of this: * - '/lib/gcli/types/fileparser.js', the default web version that uses XHR to * talk to the node server * - '/lib/server/gcli/types/fileparser.js', an NodeJS stub, and ... * - '/mozilla/gcli/types/fileparser.js', the Firefox implementation both of * these are shims which import * - 'util/fileparser', which does the real work, except the actual file access * * The file access comes from the 'util/filesystem' module, and there are 2 * implementations of this: * - '/lib/server/util/filesystem.js', which uses NodeJS APIs * - '/mozilla/util/filesystem.js', which uses OS.File APIs */ var fileparser = require('gcli/types/fileparser'); var Conversion = require('gcli/types').Conversion; exports.items = [ { item: 'type', name: 'file', filetype: 'any', // One of 'file', 'directory', 'any' existing: 'maybe', // Should be one of 'yes', 'no', 'maybe' matches: undefined, // RegExp to match the file part of the path isSelection: true, // It's not really a selection, but acts like one constructor: function() { if (this.filetype !== 'any' && this.filetype !== 'file' && this.filetype !== 'directory') { throw new Error('filetype must be one of [any|file|directory]'); } if (this.existing !== 'yes' && this.existing !== 'no' && this.existing !== 'maybe') { throw new Error('existing must be one of [yes|no|maybe]'); } }, stringify: function(file) { if (file == null) { return ''; } return file.toString(); }, parse: function(arg, context) { var options = { filetype: this.filetype, existing: this.existing, matches: this.matches }; var promise = fileparser.parse(arg.text, options); return promise.then(function(reply) { return new Conversion(reply.value, arg, reply.status, reply.message, reply.predictor); }); } } ]; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/types/fileparser', ['require', 'exports', 'module' , 'util/fileparser'], function(require, exports, module) { 'use strict'; var fileparser = require('util/fileparser'); fileparser.supportsPredictions = false; exports.parse = fileparser.parse; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('util/fileparser', ['require', 'exports', 'module' , 'util/util', 'util/l10n', 'util/spell', 'util/filesystem', 'gcli/types'], function(require, exports, module) { 'use strict'; var util = require('util/util'); var l10n = require('util/l10n'); var spell = require('util/spell'); var filesystem = require('util/filesystem'); var Status = require('gcli/types').Status; /* * An implementation of the functions that call the filesystem, designed to * support the file type. * See: lib/gcli/util/filesystem.js */ /** * Helper for the parse() function from the file type. * See util/filesystem.js for details */ exports.parse = function(typed, options) { return filesystem.stat(typed).then(function(stats) { // The 'save-as' case - the path should not exist but does if (options.existing === 'no' && stats.exists) { return { value: undefined, status: Status.INCOMPLETE, message: l10n.lookupFormat('fileErrExists', [ typed ]), predictor: undefined // No predictions that we can give here }; } if (stats.exists) { // The path exists - check it's the correct file type ... if (options.filetype === 'file' && !stats.isFile) { return { value: undefined, status: Status.INCOMPLETE, message: l10n.lookupFormat('fileErrIsNotFile', [ typed ]), predictor: getPredictor(typed, options) }; } if (options.filetype === 'directory' && !stats.isDir) { return { value: undefined, status: Status.INCOMPLETE, message: l10n.lookupFormat('fileErrIsNotDirectory', [ typed ]), predictor: getPredictor(typed, options) }; } // ... and that it matches any 'match' RegExp if (options.matches != null && !options.matches.test(typed)) { return { value: undefined, status: Status.INCOMPLETE, message: l10n.lookupFormat('fileErrDoesntMatch', [ typed, options.source ]), predictor: getPredictor(typed, options) }; } } else { if (options.existing === 'yes') { // We wanted something that exists, but it doesn't. But we don't know // if the path so far is an ERROR or just INCOMPLETE var parentName = filesystem.dirname(typed); return filesystem.stat(parentName).then(function(stats) { return { value: undefined, status: stats.isDir ? Status.INCOMPLETE : Status.ERROR, message: l10n.lookupFormat('fileErrNotExists', [ typed ]), predictor: getPredictor(typed, options) }; }); } } // We found no problems return { value: typed, status: Status.VALID, message: undefined, predictor: getPredictor(typed, options) }; }); }; var RANK_OPTIONS = { noSort: true, prefixZero: true }; /** * We want to be able to turn predictions off in Firefox */ exports.supportsPredictions = true; /** * Get a function which creates predictions of files that match the given * path */ function getPredictor(typed, options) { if (!exports.supportsPredictions) { return undefined; } return function() { var allowFile = (options.filetype !== 'directory'); var parts = filesystem.split(typed); var absolute = (typed.indexOf('/') === 0); var roots; if (absolute) { roots = [ { name: '/', dist: 0, original: '/' } ]; } else { roots = history.getCommonDirectories().map(function(root) { return { name: root, dist: 0, original: root }; }); } // Add each part of the typed pathname onto each of the roots in turn, // Finding options from each of those paths, and using these options as // our roots for the next part var partsAdded = util.promiseEach(parts, function(part, index) { var partsSoFar = filesystem.join.apply(filesystem, parts.slice(0, index + 1)); // We allow this file matches in this pass if we're allowed files at all // (i.e this isn't 'cd') and if this is the last part of the path var allowFileForPart = (allowFile && index >= parts.length - 1); var rootsPromise = util.promiseEach(roots, function(root) { // Extend each roots to a list of all the files in each of the roots var matchFile = allowFileForPart ? options.matches : null; var promise = filesystem.ls(root.name, matchFile); var onSuccess = function(entries) { // Unless this is the final part filter out the non-directories if (!allowFileForPart) { entries = entries.filter(function(entry) { return entry.isDir; }); } var entryMap = {}; entries.forEach(function(entry) { entryMap[entry.pathname] = entry; }); return entryMap; }; var onError = function(err) { // We expect errors due to the path not being a directory, not being // accessible, or removed since the call to 'readdir', but other // errors should be reported var noComplainCodes = [ 'ENOTDIR', 'EACCES', 'EBADF', 'ENOENT' ]; if (noComplainCodes.indexOf(err.code) === -1) { console.error('Error looing up', root.name, err); } return {}; }; promise = promise.then(onSuccess, onError); // We want to compare all the directory entries with the original root // plus the partsSoFar var compare = filesystem.join(root.original, partsSoFar); return promise.then(function(entryMap) { var ranks = spell.rank(compare, Object.keys(entryMap), RANK_OPTIONS); // penalize each path by the distance of it's parent ranks.forEach(function(rank) { rank.original = root.original; rank.stats = entryMap[rank.name]; }); return ranks; }); }); return rootsPromise.then(function(data) { // data is an array of arrays of ranking objects. Squash down. data = data.reduce(function(prev, curr) { return prev.concat(curr); }, []); data.sort(function(r1, r2) { return r1.dist - r2.dist; }); // Trim, but by how many? // If this is the last run through, we want to present the user with // a sensible set of predictions. Otherwise we want to trim the tree // to a reasonable set of matches, so we're happy with 1 // We look through x +/- 3 roots, and find the one with the biggest // distance delta, and cut below that // x=5 for the last time through, and x=8 otherwise var isLast = index >= parts.length - 1; var start = isLast ? 1 : 5; var end = isLast ? 7 : 10; var maxDeltaAt = start; var maxDelta = data[start].dist - data[start - 1].dist; for (var i = start + 1; i < end; i++) { var delta = data[i].dist - data[i - 1].dist; if (delta >= maxDelta) { maxDelta = delta; maxDeltaAt = i; } } // Update the list of roots for the next time round roots = data.slice(0, maxDeltaAt); }); }); return partsAdded.then(function() { var predictions = roots.map(function(root) { var isFile = root.stats && root.stats.isFile; var isDir = root.stats && root.stats.isDir; var name = root.name; if (isDir && name.charAt(name.length) !== filesystem.sep) { name += filesystem.sep; } return { name: name, incomplete: !(allowFile && isFile), isFile: isFile, // Added for describe, below dist: root.dist, // TODO: Remove - added for debug in describe }; }); return util.promiseEach(predictions, function(prediction) { if (!prediction.isFile) { prediction.description = '(' + prediction.dist + ')'; prediction.dist = undefined; prediction.isFile = undefined; return prediction; } return filesystem.describe(prediction.name).then(function(description) { prediction.description = description; prediction.dist = undefined; prediction.isFile = undefined; return prediction; }); }); }); }; } // ============================================================================= /* * The idea is that we maintain a list of 'directories that the user is * interested in'. We store directories in a most-frequently-used cache * of some description. * But for now we're just using / and ~/ */ var history = { getCommonDirectories: function() { return [ filesystem.sep, // i.e. the root directory filesystem.home // i.e. the users home directory ]; }, addCommonDirectory: function(ignore) { // Not implemented yet } }; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('util/filesystem', ['require', 'exports', 'module' , 'util/promise'], function(require, exports, module) { 'use strict'; var OS = Components.utils.import('resource://gre/modules/osfile.jsm', {}).OS; var promise = require('util/promise'); /** * A set of functions that don't really belong in 'fs' (because they're not * really universal in scope) but also kind of do (because they're not specific * to GCLI */ exports.join = OS.Path.join; exports.sep = OS.Path.sep; exports.dirname = OS.Path.dirname; var dirService = Components.classes['@mozilla.org/file/directory_service;1'] .getService(Components.interfaces.nsIProperties); exports.home = dirService.get('Home', Components.interfaces.nsIFile).path; if ('winGetDrive' in OS.Path) { exports.sep = '\\'; } else { exports.sep = '/'; } /** * Split a path into its components. * @param pathname (string) The part to cut up * @return An array of path components */ exports.split = function(pathname) { return OS.Path.split(pathname).components; }; /** * @param pathname string, path of an existing directory * @param matches optional regular expression - filter output to include only * the files that match the regular expression. The regexp is applied to the * filename only not to the full path * @return A promise of an array of stat objects for each member of the * directory pointed to by ``pathname``, each containing 2 extra properties: * - pathname: The full pathname of the file * - filename: The final filename part of the pathname */ exports.ls = function(pathname, matches) { var iterator = new OS.File.DirectoryIterator(pathname); var entries = []; var iteratePromise = iterator.forEach(function(entry) { entries.push({ exists: true, isDir: entry.isDir, isFile: !entry.isFile, filename: entry.name, pathname: entry.path }); }); return iteratePromise.then(function onSuccess() { iterator.close(); return entries; }, function onFailure(reason) { iterator.close(); throw reason; } ); }; /** * stat() is annoying because it considers stat('/doesnt/exist') to be an * error, when the point of stat() is to *find* *out*. So this wrapper just * converts 'ENOENT' i.e. doesn't exist to { exists:false } and adds * exists:true to stat blocks from existing paths */ exports.stat = function(pathname) { var onResolve = function(stats) { return { exists: true, isDir: stats.isDir, isFile: !stats.isFile }; }; var onReject = function(err) { if (err instanceof OS.File.Error && err.becauseNoSuchFile) { return { exists: false, isDir: false, isFile: false }; } throw err; }; return OS.File.stat(pathname).then(onResolve, onReject); }; /** * We may read the first line of a file to describe it? * Right now, however, we do nothing. */ exports.describe = function(pathname) { return promise.resolve(''); }; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/types/javascript', ['require', 'exports', 'module' , 'util/promise', 'util/l10n', 'gcli/types'], function(require, exports, module) { 'use strict'; var promise = require('util/promise'); var l10n = require('util/l10n'); var types = require('gcli/types'); var Conversion = types.Conversion; var Type = types.Type; var Status = types.Status; /** * The object against which we complete, which is usually 'window' if it exists * but could be something else in non-web-content environments. */ var globalObject; if (typeof window !== 'undefined') { globalObject = window; } /** * Setter for the object against which JavaScript completions happen */ exports.setGlobalObject = function(obj) { globalObject = obj; }; /** * Getter for the object against which JavaScript completions happen, for use * in testing */ exports.getGlobalObject = function() { return globalObject; }; /** * Remove registration of object against which JavaScript completions happen */ exports.unsetGlobalObject = function() { globalObject = undefined; }; /** * 'javascript' handles scripted input */ function JavascriptType(typeSpec) { } JavascriptType.prototype = Object.create(Type.prototype); JavascriptType.prototype.stringify = function(value, context) { if (value == null) { return ''; } return value; }; /** * When sorting out completions, there is no point in displaying millions of * matches - this the number of matches that we aim for */ JavascriptType.MAX_COMPLETION_MATCHES = 10; JavascriptType.prototype.parse = function(arg, context) { var typed = arg.text; var scope = globalObject; // No input is undefined if (typed === '') { return promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE)); } // Just accept numbers if (!isNaN(parseFloat(typed)) && isFinite(typed)) { return promise.resolve(new Conversion(typed, arg)); } // Just accept constants like true/false/null/etc if (typed.trim().match(/(null|undefined|NaN|Infinity|true|false)/)) { return promise.resolve(new Conversion(typed, arg)); } // Analyze the input text and find the beginning of the last part that // should be completed. var beginning = this._findCompletionBeginning(typed); // There was an error analyzing the string. if (beginning.err) { return promise.resolve(new Conversion(typed, arg, Status.ERROR, beginning.err)); } // If the current state is ParseState.COMPLEX, then we can't do completion. // so bail out now if (beginning.state === ParseState.COMPLEX) { return promise.resolve(new Conversion(typed, arg)); } // If the current state is not ParseState.NORMAL, then we are inside of a // string which means that no completion is possible. if (beginning.state !== ParseState.NORMAL) { return promise.resolve(new Conversion(typed, arg, Status.INCOMPLETE, '')); } var completionPart = typed.substring(beginning.startPos); var properties = completionPart.split('.'); var matchProp; var prop; if (properties.length > 1) { matchProp = properties.pop().trimLeft(); for (var i = 0; i < properties.length; i++) { prop = properties[i].trim(); // We can't complete on null.foo, so bail out if (scope == null) { return promise.resolve(new Conversion(typed, arg, Status.ERROR, l10n.lookup('jstypeParseScope'))); } if (prop === '') { return promise.resolve(new Conversion(typed, arg, Status.INCOMPLETE, '')); } // Check if prop is a getter function on 'scope'. Functions can change // other stuff so we can't execute them to get the next object. Stop here. if (this._isSafeProperty(scope, prop)) { return promise.resolve(new Conversion(typed, arg)); } try { scope = scope[prop]; } catch (ex) { // It would be nice to be able to report this error in some way but // as it can happen just when someone types '{sessionStorage.', it // almost doesn't really count as an error, so we ignore it return promise.resolve(new Conversion(typed, arg, Status.VALID, '')); } } } else { matchProp = properties[0].trimLeft(); } // If the reason we just stopped adjusting the scope was a non-simple string, // then we're not sure if the input is valid or invalid, so accept it if (prop && !prop.match(/^[0-9A-Za-z]*$/)) { return promise.resolve(new Conversion(typed, arg)); } // However if the prop was a simple string, it is an error if (scope == null) { var msg = l10n.lookupFormat('jstypeParseMissing', [ prop ]); return promise.resolve(new Conversion(typed, arg, Status.ERROR, msg)); } // If the thing we're looking for isn't a simple string, then we're not going // to find it, but we're not sure if it's valid or invalid, so accept it if (!matchProp.match(/^[0-9A-Za-z]*$/)) { return promise.resolve(new Conversion(typed, arg)); } // Skip Iterators and Generators. if (this._isIteratorOrGenerator(scope)) { return promise.resolve(new Conversion(typed, arg)); } var matchLen = matchProp.length; var prefix = matchLen === 0 ? typed : typed.slice(0, -matchLen); var status = Status.INCOMPLETE; var message = ''; // We really want an array of matches (for sorting) but it's easier to // detect existing members if we're using a map initially var matches = {}; // We only display a maximum of MAX_COMPLETION_MATCHES, so there is no point // in digging up the prototype chain for matches that we're never going to // use. Initially look for matches directly on the object itself and then // look up the chain to find more var distUpPrototypeChain = 0; var root = scope; try { while (root != null && Object.keys(matches).length < JavascriptType.MAX_COMPLETION_MATCHES) { Object.keys(root).forEach(function(property) { // Only add matching properties. Also, as we're walking up the // prototype chain, properties on 'higher' prototypes don't override // similarly named properties lower down if (property.indexOf(matchProp) === 0 && !(property in matches)) { matches[property] = { prop: property, distUpPrototypeChain: distUpPrototypeChain }; } }); distUpPrototypeChain++; root = Object.getPrototypeOf(root); } } catch (ex) { return promise.resolve(new Conversion(typed, arg, Status.INCOMPLETE, '')); } // Convert to an array for sorting, and while we're at it, note if we got // an exact match so we know that this input is valid matches = Object.keys(matches).map(function(property) { if (property === matchProp) { status = Status.VALID; } return matches[property]; }); // The sort keys are: // - Being on the object itself, not in the prototype chain // - The lack of existence of a vendor prefix // - The name matches.sort(function(m1, m2) { if (m1.distUpPrototypeChain !== m2.distUpPrototypeChain) { return m1.distUpPrototypeChain - m2.distUpPrototypeChain; } // Push all vendor prefixes to the bottom of the list return isVendorPrefixed(m1.prop) ? (isVendorPrefixed(m2.prop) ? m1.prop.localeCompare(m2.prop) : 1) : (isVendorPrefixed(m2.prop) ? -1 : m1.prop.localeCompare(m2.prop)); }); // Trim to size. There is a bug for doing a better job of finding matches // (bug 682694), but in the mean time there is a performance problem // associated with creating a large number of DOM nodes that few people will // ever read, so trim ... if (matches.length > JavascriptType.MAX_COMPLETION_MATCHES) { matches = matches.slice(0, JavascriptType.MAX_COMPLETION_MATCHES - 1); } // Decorate the matches with: // - a description // - a value (for the menu) and, // - an incomplete flag which reports if we should assume that the user isn't // going to carry on the JS expression with this input so far var predictions = matches.map(function(match) { var description; var incomplete = true; if (this._isSafeProperty(scope, match.prop)) { description = '(property getter)'; } else { try { var value = scope[match.prop]; if (typeof value === 'function') { description = '(function)'; } else if (typeof value === 'boolean' || typeof value === 'number') { description = '= ' + value; incomplete = false; } else if (typeof value === 'string') { if (value.length > 40) { value = value.substring(0, 37) + 'ā€¦'; } description = '= \'' + value + '\''; incomplete = false; } else { description = '(' + typeof value + ')'; } } catch (ex) { description = '(' + l10n.lookup('jstypeParseError') + ')'; } } return { name: prefix + match.prop, value: { name: prefix + match.prop, description: description }, description: description, incomplete: incomplete }; }, this); if (predictions.length === 0) { status = Status.ERROR; message = l10n.lookupFormat('jstypeParseMissing', [ matchProp ]); } // If the match is the only one possible, and its VALID, predict nothing if (predictions.length === 1 && status === Status.VALID) { predictions = []; } return promise.resolve(new Conversion(typed, arg, status, message, promise.resolve(predictions))); }; /** * Does the given property have a prefix that indicates that it is vendor * specific? */ function isVendorPrefixed(name) { return name.indexOf('moz') === 0 || name.indexOf('webkit') === 0 || name.indexOf('ms') === 0; } /** * Constants used in return value of _findCompletionBeginning() */ var ParseState = { /** * We have simple input like window.foo, without any punctuation that makes * completion prediction be confusing or wrong */ NORMAL: 0, /** * The cursor is in some Javascript that makes completion hard to predict, * like console.log( */ COMPLEX: 1, /** * The cursor is inside single quotes (') */ QUOTE: 2, /** * The cursor is inside single quotes (") */ DQUOTE: 3 }; var OPEN_BODY = '{[('.split(''); var CLOSE_BODY = '}])'.split(''); var OPEN_CLOSE_BODY = { '{': '}', '[': ']', '(': ')' }; /** * How we distinguish between simple and complex JS input. We attempt * completion against simple JS. */ var simpleChars = /[a-zA-Z0-9.]/; /** * Analyzes a given string to find the last statement that is interesting for * later completion. * @param text A string to analyze * @return If there was an error in the string detected, then a object like * { err: 'ErrorMesssage' } * is returned, otherwise a object like * { * state: ParseState.NORMAL|ParseState.QUOTE|ParseState.DQUOTE, * startPos: index of where the last statement begins * } */ JavascriptType.prototype._findCompletionBeginning = function(text) { var bodyStack = []; var state = ParseState.NORMAL; var start = 0; var c; var complex = false; for (var i = 0; i < text.length; i++) { c = text[i]; if (!simpleChars.test(c)) { complex = true; } switch (state) { // Normal JS state. case ParseState.NORMAL: if (c === '"') { state = ParseState.DQUOTE; } else if (c === '\'') { state = ParseState.QUOTE; } else if (c === ';') { start = i + 1; } else if (c === ' ') { start = i + 1; } else if (OPEN_BODY.indexOf(c) != -1) { bodyStack.push({ token: c, start: start }); start = i + 1; } else if (CLOSE_BODY.indexOf(c) != -1) { var last = bodyStack.pop(); if (!last || OPEN_CLOSE_BODY[last.token] != c) { return { err: l10n.lookup('jstypeBeginSyntax') }; } if (c === '}') { start = i + 1; } else { start = last.start; } } break; // Double quote state > " < case ParseState.DQUOTE: if (c === '\\') { i ++; } else if (c === '\n') { return { err: l10n.lookup('jstypeBeginUnterm') }; } else if (c === '"') { state = ParseState.NORMAL; } break; // Single quote state > ' < case ParseState.QUOTE: if (c === '\\') { i ++; } else if (c === '\n') { return { err: l10n.lookup('jstypeBeginUnterm') }; } else if (c === '\'') { state = ParseState.NORMAL; } break; } } if (state === ParseState.NORMAL && complex) { state = ParseState.COMPLEX; } return { state: state, startPos: start }; }; /** * Return true if the passed object is either an iterator or a generator, and * false otherwise * @param obj The object to check */ JavascriptType.prototype._isIteratorOrGenerator = function(obj) { if (obj === null) { return false; } if (typeof aObject === 'object') { if (typeof obj.__iterator__ === 'function' || obj.constructor && obj.constructor.name === 'Iterator') { return true; } try { var str = obj.toString(); if (typeof obj.next === 'function' && str.indexOf('[object Generator') === 0) { return true; } } catch (ex) { // window.history.next throws in the typeof check above. return false; } } return false; }; /** * Would calling 'scope[prop]' cause the invocation of a non-native (i.e. user * defined) function property? * Since calling functions can have side effects, it's only safe to do that if * explicitly requested, rather than because we're trying things out for the * purposes of completion. */ JavascriptType.prototype._isSafeProperty = function(scope, prop) { if (typeof scope !== 'object') { return false; } // Walk up the prototype chain of 'scope' looking for a property descriptor // for 'prop' var propDesc; while (scope) { try { propDesc = Object.getOwnPropertyDescriptor(scope, prop); if (propDesc) { break; } } catch (ex) { // Native getters throw here. See bug 520882. if (ex.name === 'NS_ERROR_XPC_BAD_CONVERT_JS' || ex.name === 'NS_ERROR_XPC_BAD_OP_ON_WN_PROTO') { return false; } return true; } scope = Object.getPrototypeOf(scope); } if (!propDesc) { return false; } if (!propDesc.get) { return false; } // The property is safe if 'get' isn't a function or if the function has a // prototype (in which case it's native) return typeof propDesc.get !== 'function' || 'prototype' in propDesc.get; }; JavascriptType.prototype.name = 'javascript'; exports.items = [ JavascriptType ]; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/types/node', ['require', 'exports', 'module' , 'util/promise', 'util/host', 'util/l10n', 'util/util', 'gcli/types', 'gcli/argument'], function(require, exports, module) { 'use strict'; var promise = require('util/promise'); var Highlighter = require('util/host').Highlighter; var l10n = require('util/l10n'); var util = require('util/util'); var Status = require('gcli/types').Status; var Conversion = require('gcli/types').Conversion; var BlankArgument = require('gcli/argument').BlankArgument; /** * The object against which we complete, which is usually 'window' if it exists * but could be something else in non-web-content environments. */ var doc; if (typeof document !== 'undefined') { doc = document; } /** * For testing only. * The fake empty NodeList used when there are no matches, we replace this with * something that looks better as soon as we have a document, so not only * should you not use this, but you shouldn't cache it either. */ exports._empty = []; /** * Setter for the document that contains the nodes we're matching */ exports.setDocument = function(document) { doc = document; if (doc != null) { exports._empty = util.createEmptyNodeList(doc); } }; /** * Undo the effects of setDocument() */ exports.unsetDocument = function() { doc = undefined; exports._empty = undefined; }; /** * Getter for the document that contains the nodes we're matching * Most for changing things back to how they were for unit testing */ exports.getDocument = function() { return doc; }; /** * Helper functions to be attached to the prototypes of NodeType and * NodeListType to allow inputter to tell us which nodes should be highlighted */ function onEnter(assignment) { assignment.highlighter = new Highlighter(doc); assignment.highlighter.nodelist = assignment.conversion.matches; } /** @see #onEnter() */ function onLeave(assignment) { if (!assignment.highlighter) { return; } assignment.highlighter.destroy(); delete assignment.highlighter; } /** @see #onEnter() */ function onChange(assignment) { if (assignment.conversion.matches == null) { return; } if (!assignment.highlighter) { return; } assignment.highlighter.nodelist = assignment.conversion.matches; } /** * The exported 'node' and 'nodelist' types */ exports.items = [ { // The 'node' type is a CSS expression that refers to a single node item: 'type', name: 'node', stringify: function(value, context) { if (value == null) { return ''; } return value.__gcliQuery || 'Error'; }, parse: function(arg, context) { var reply; if (arg.text === '') { reply = new Conversion(undefined, arg, Status.INCOMPLETE); reply.matches = util.createEmptyNodeList(doc); } else { var nodes; try { nodes = doc.querySelectorAll(arg.text); if (nodes.length === 0) { reply = new Conversion(undefined, arg, Status.INCOMPLETE, l10n.lookup('nodeParseNone')); } else if (nodes.length === 1) { var node = nodes.item(0); node.__gcliQuery = arg.text; reply = new Conversion(node, arg, Status.VALID, ''); } else { var msg = l10n.lookupFormat('nodeParseMultiple', [ nodes.length ]); reply = new Conversion(undefined, arg, Status.ERROR, msg); } reply.matches = nodes; } catch (ex) { reply = new Conversion(undefined, arg, Status.ERROR, l10n.lookup('nodeParseSyntax')); } } return promise.resolve(reply); }, onEnter: onEnter, onLeave: onLeave, onChange: onChange }, { // The 'nodelist' type is a CSS expression that refers to a node list item: 'type', name: 'nodelist', // The 'allowEmpty' option ensures that we do not complain if the entered // CSS selector is valid, but does not match any nodes. There is some // overlap between this option and 'defaultValue'. What the user wants, in // most cases, would be to use 'defaultText' (i.e. what is typed rather than // the value that it represents). However this isn't a concept that exists // yet and should probably be a part of GCLI if/when it does. // All NodeListTypes have an automatic defaultValue of an empty NodeList so // they can easily be used in named parameters. allowEmpty: false, constructor: function() { if (typeof this.allowEmpty !== 'boolean') { throw new Error('Legal values for allowEmpty are [true|false]'); } }, getBlank: function(context) { return new Conversion(exports._empty, new BlankArgument(), Status.VALID); }, stringify: function(value, context) { if (value == null) { return ''; } return value.__gcliQuery || 'Error'; }, parse: function(arg, context) { var reply; try { if (arg.text === '') { reply = new Conversion(undefined, arg, Status.INCOMPLETE); reply.matches = util.createEmptyNodeList(doc); } else { var nodes = doc.querySelectorAll(arg.text); if (nodes.length === 0 && !this.allowEmpty) { reply = new Conversion(undefined, arg, Status.INCOMPLETE, l10n.lookup('nodeParseNone')); } else { reply = new Conversion(nodes, arg, Status.VALID, ''); } reply.matches = nodes; } } catch (ex) { reply = new Conversion(undefined, arg, Status.ERROR, l10n.lookup('nodeParseSyntax')); reply.matches = util.createEmptyNodeList(doc); } return promise.resolve(reply); }, onEnter: onEnter, onLeave: onLeave, onChange: onChange } ]; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('util/host', ['require', 'exports', 'module' , 'util/util'], function(require, exports, module) { 'use strict'; var util = require('util/util'); /** * The chromeWindow as as required by Highlighter, so it knows where to * create temporary highlight nodes. */ exports.chromeWindow = undefined; function Highlighter(document) { this._document = document; this._nodes = util.createEmptyNodeList(this._document); } Object.defineProperty(Highlighter.prototype, 'nodelist', { set: function(nodes) { Array.prototype.forEach.call(this._nodes, this._unhighlightNode, this); this._nodes = (nodes == null) ? util.createEmptyNodeList(this._document) : nodes; Array.prototype.forEach.call(this._nodes, this._highlightNode, this); }, get: function() { return this._nodes; }, enumerable: true }); Highlighter.prototype.destroy = function() { this.nodelist = null; }; Highlighter.prototype._highlightNode = function(node) { // Enable when the highlighter rewrite is done }; Highlighter.prototype._unhighlightNode = function(node) { // Enable when the highlighter rewrite is done }; exports.Highlighter = Highlighter; /** * See docs in lib/util/host.js:exec */ exports.exec = function(execSpec) { throw new Error('Not supported'); }; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/types/number', ['require', 'exports', 'module' , 'util/promise', 'util/l10n', 'gcli/types'], function(require, exports, module) { 'use strict'; var promise = require('util/promise'); var l10n = require('util/l10n'); var Status = require('gcli/types').Status; var Conversion = require('gcli/types').Conversion; exports.items = [ { // 'number' type // Has custom max / min / step values to control increment and decrement // and a boolean allowFloat property to clamp values to integers item: 'type', name: 'number', allowFloat: false, max: undefined, min: undefined, step: 1, constructor: function() { if (!this.allowFloat && (this._isFloat(this.min) || this._isFloat(this.max) || this._isFloat(this.step))) { throw new Error('allowFloat is false, but non-integer values given in type spec'); } }, stringify: function(value, context) { if (value == null) { return ''; } return '' + value; }, getMin: function(context) { if (this.min) { if (typeof this.min === 'function') { return this.min(context); } if (typeof this.min === 'number') { return this.min; } } return undefined; }, getMax: function(context) { if (this.max) { if (typeof this.max === 'function') { return this.max(context); } if (typeof this.max === 'number') { return this.max; } } return undefined; }, parse: function(arg, context) { var msg; if (arg.text.replace(/^\s*-?/, '').length === 0) { return promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE, '')); } if (!this.allowFloat && (arg.text.indexOf('.') !== -1)) { msg = l10n.lookupFormat('typesNumberNotInt2', [ arg.text ]); return promise.resolve(new Conversion(undefined, arg, Status.ERROR, msg)); } var value; if (this.allowFloat) { value = parseFloat(arg.text); } else { value = parseInt(arg.text, 10); } if (isNaN(value)) { msg = l10n.lookupFormat('typesNumberNan', [ arg.text ]); return promise.resolve(new Conversion(undefined, arg, Status.ERROR, msg)); } var max = this.getMax(context); if (max != null && value > max) { msg = l10n.lookupFormat('typesNumberMax', [ value, max ]); return promise.resolve(new Conversion(undefined, arg, Status.ERROR, msg)); } var min = this.getMin(context); if (min != null && value < min) { msg = l10n.lookupFormat('typesNumberMin', [ value, min ]); return promise.resolve(new Conversion(undefined, arg, Status.ERROR, msg)); } return promise.resolve(new Conversion(value, arg)); }, decrement: function(value, context) { if (typeof value !== 'number' || isNaN(value)) { return this.getMax(context) || 1; } var newValue = value - this.step; // Snap to the nearest incremental of the step newValue = Math.ceil(newValue / this.step) * this.step; return this._boundsCheck(newValue, context); }, increment: function(value, context) { if (typeof value !== 'number' || isNaN(value)) { var min = this.getMin(context); return min != null ? min : 0; } var newValue = value + this.step; // Snap to the nearest incremental of the step newValue = Math.floor(newValue / this.step) * this.step; if (this.getMax(context) == null) { return newValue; } return this._boundsCheck(newValue, context); }, // Return the input value so long as it is within the max/min bounds. // If it is lower than the minimum, return the minimum. If it is bigger // than the maximum then return the maximum. _boundsCheck: function(value, context) { var min = this.getMin(context); if (min != null && value < min) { return min; } var max = this.getMax(context); if (max != null && value > max) { return max; } return value; }, // Return true if the given value is a finite number and not an integer, // else return false. _isFloat: function(value) { return ((typeof value === 'number') && isFinite(value) && (value % 1 !== 0)); } } ]; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/types/resource', ['require', 'exports', 'module' , 'util/promise', 'gcli/types/selection'], function(require, exports, module) { 'use strict'; var promise = require('util/promise'); var SelectionType = require('gcli/types/selection').SelectionType; exports.clearResourceCache = function() { ResourceCache.clear(); }; /** * The object against which we complete, which is usually 'window' if it exists * but could be something else in non-web-content environments. */ var doc; if (typeof document !== 'undefined') { doc = document; } /** * Setter for the document that contains the nodes we're matching */ exports.setDocument = function(document) { doc = document; }; /** * Undo the effects of setDocument() */ exports.unsetDocument = function() { ResourceCache.clear(); doc = undefined; }; /** * Getter for the document that contains the nodes we're matching * Most for changing things back to how they were for unit testing */ exports.getDocument = function() { return doc; }; /** * Resources are bits of CSS and JavaScript that the page either includes * directly or as a result of reading some remote resource. * Resource should not be used directly, but instead through a sub-class like * CssResource or ScriptResource. */ function Resource(name, type, inline, element) { this.name = name; this.type = type; this.inline = inline; this.element = element; } /** * Get the contents of the given resource as a string. * The base Resource leaves this unimplemented. */ Resource.prototype.getContents = function() { throw new Error('not implemented'); }; Resource.TYPE_SCRIPT = 'text/javascript'; Resource.TYPE_CSS = 'text/css'; /** * A CssResource provides an implementation of Resource that works for both * [style] elements and [link type='text/css'] elements in the [head]. */ function CssResource(domSheet) { this.name = domSheet.href; if (!this.name) { this.name = domSheet.ownerNode.id ? 'css#' + domSheet.ownerNode.id : 'inline-css'; } this.inline = (domSheet.href == null); this.type = Resource.TYPE_CSS; this.element = domSheet; } CssResource.prototype = Object.create(Resource.prototype); CssResource.prototype.loadContents = function(callback) { callback(this.element.ownerNode.innerHTML); }; CssResource._getAllStyles = function() { var resources = []; if (doc == null) { return resources; } Array.prototype.forEach.call(doc.styleSheets, function(domSheet) { CssResource._getStyle(domSheet, resources); }); dedupe(resources, function(clones) { for (var i = 0; i < clones.length; i++) { clones[i].name = clones[i].name + '-' + i; } }); return resources; }; CssResource._getStyle = function(domSheet, resources) { var resource = ResourceCache.get(domSheet); if (!resource) { resource = new CssResource(domSheet); ResourceCache.add(domSheet, resource); } resources.push(resource); // Look for imported stylesheets try { Array.prototype.forEach.call(domSheet.cssRules, function(domRule) { if (domRule.type == CSSRule.IMPORT_RULE && domRule.styleSheet) { CssResource._getStyle(domRule.styleSheet, resources); } }, this); } catch (ex) { // For system stylesheets } }; /** * A ScriptResource provides an implementation of Resource that works for * [script] elements (both with a src attribute, and used directly). */ function ScriptResource(scriptNode) { this.name = scriptNode.src; if (!this.name) { this.name = scriptNode.id ? 'script#' + scriptNode.id : 'inline-script'; } this.inline = (scriptNode.src === '' || scriptNode.src == null); this.type = Resource.TYPE_SCRIPT; this.element = scriptNode; } ScriptResource.prototype = Object.create(Resource.prototype); ScriptResource.prototype.loadContents = function(callback) { if (this.inline) { callback(this.element.innerHTML); } else { // It would be good if there was a better way to get the script source var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState !== xhr.DONE) { return; } callback(xhr.responseText); }; xhr.open('GET', this.element.src, true); xhr.send(); } }; ScriptResource._getAllScripts = function() { if (doc == null) { return []; } var scriptNodes = doc.querySelectorAll('script'); var resources = Array.prototype.map.call(scriptNodes, function(scriptNode) { var resource = ResourceCache.get(scriptNode); if (!resource) { resource = new ScriptResource(scriptNode); ResourceCache.add(scriptNode, resource); } return resource; }); dedupe(resources, function(clones) { for (var i = 0; i < clones.length; i++) { clones[i].name = clones[i].name + '-' + i; } }); return resources; }; /** * Find resources with the same name, and call onDupe to change the names */ function dedupe(resources, onDupe) { // first create a map of name->[array of resources with same name] var names = {}; resources.forEach(function(scriptResource) { if (names[scriptResource.name] == null) { names[scriptResource.name] = []; } names[scriptResource.name].push(scriptResource); }); // Call the de-dupe function for each set of dupes Object.keys(names).forEach(function(name) { var clones = names[name]; if (clones.length > 1) { onDupe(clones); } }); } /** * A quick cache of resources against nodes * TODO: Potential memory leak when the target document has css or script * resources repeatedly added and removed. Solution might be to use a weak * hash map or some such. */ var ResourceCache = { _cached: [], /** * Do we already have a resource that was created for the given node */ get: function(node) { for (var i = 0; i < ResourceCache._cached.length; i++) { if (ResourceCache._cached[i].node === node) { return ResourceCache._cached[i].resource; } } return null; }, /** * Add a resource for a given node */ add: function(node, resource) { ResourceCache._cached.push({ node: node, resource: resource }); }, /** * Drop all cache entries. Helpful to prevent memory leaks */ clear: function() { ResourceCache._cached = []; } }; /** * The resource type itself */ exports.items = [ { item: 'type', constructor: function() { if (this.include !== Resource.TYPE_SCRIPT && this.include !== Resource.TYPE_CSS && this.include != null) { throw new Error('invalid include property: ' + this.include); } }, name: 'resource', parent: 'selection', include: null, cacheable: false, lookup: function() { var resources = []; if (this.include !== Resource.TYPE_SCRIPT) { Array.prototype.push.apply(resources, CssResource._getAllStyles()); } if (this.include !== Resource.TYPE_CSS) { Array.prototype.push.apply(resources, ScriptResource._getAllScripts()); } return promise.resolve(resources.map(function(resource) { return { name: resource.name, value: resource }; })); } } ]; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/types/setting', ['require', 'exports', 'module' , 'gcli/settings', 'gcli/types'], function(require, exports, module) { 'use strict'; var settings = require('gcli/settings'); var types = require('gcli/types'); exports.items = [ { // A type for selecting a known setting item: 'type', name: 'setting', parent: 'selection', cacheable: true, constructor: function() { settings.onChange.add(function(ev) { this.clearCache(); }, this); }, lookup: function() { return settings.getAll().map(function(setting) { return { name: setting.name, value: setting }; }); } }, { // A type for entering the value of a known setting // Customizations: // - settingParamName The name of the setting parameter so we can customize the // type that we are expecting to read item: 'type', name: 'settingValue', parent: 'delegate', settingParamName: 'setting', delegateType: function(context) { if (context != null) { var setting = context.getArgsObject()[this.settingParamName]; if (setting != null) { return setting.type; } } return types.createType('blank'); } } ]; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/types/string', ['require', 'exports', 'module' , 'util/promise', 'gcli/types'], function(require, exports, module) { 'use strict'; var promise = require('util/promise'); var Status = require('gcli/types').Status; var Conversion = require('gcli/types').Conversion; exports.items = [ { // 'string' the most basic string type where all we need to do is to take // care of converting escaped characters like \t, \n, etc. // For the full list see // https://developer.mozilla.org/en-US/docs/JavaScript/Guide/Values,_variables,_and_literals // The exception is that we ignore \b because replacing '\b' characters in // stringify() with their escaped version injects '\\b' all over the place // and the need to support \b seems low) // Customizations: // allowBlank: Allow a blank string to be counted as valid item: 'type', name: 'string', allowBlank: false, stringify: function(value, context) { if (value == null) { return ''; } return value .replace(/\\/g, '\\\\') .replace(/\f/g, '\\f') .replace(/\n/g, '\\n') .replace(/\r/g, '\\r') .replace(/\t/g, '\\t') .replace(/\v/g, '\\v') .replace(/\n/g, '\\n') .replace(/\r/g, '\\r') .replace(/ /g, '\\ ') .replace(/'/g, '\\\'') .replace(/"/g, '\\"') .replace(/{/g, '\\{') .replace(/}/g, '\\}'); }, parse:function(arg, context) { if (!this.allowBlank && (arg.text == null || arg.text === '')) { return promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE, '')); } // The string '\\' (i.e. an escaped \ (represented here as '\\\\' because it // is double escaped)) is first converted to a private unicode character and // then at the end from \uF000 to a single '\' to avoid the string \\n being // converted first to \n and then to a var value = arg.text .replace(/\\\\/g, '\uF000') .replace(/\\f/g, '\f') .replace(/\\n/g, '\n') .replace(/\\r/g, '\r') .replace(/\\t/g, '\t') .replace(/\\v/g, '\v') .replace(/\\n/g, '\n') .replace(/\\r/g, '\r') .replace(/\\ /g, ' ') .replace(/\\'/g, '\'') .replace(/\\"/g, '"') .replace(/\\{/g, '{') .replace(/\\}/g, '}') .replace(/\uF000/g, '\\'); return promise.resolve(new Conversion(value, arg)); } } ]; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/converters/basic', ['require', 'exports', 'module' , 'util/util'], function(require, exports, module) { 'use strict'; var util = require('util/util'); /** * Several converters are just data.toString inside a 'p' element */ function nodeFromDataToString(data, conversionContext) { var node = util.createElement(conversionContext.document, 'p'); node.textContent = data.toString(); return node; } exports.items = [ { item: 'converter', from: 'string', to: 'dom', exec: nodeFromDataToString }, { item: 'converter', from: 'number', to: 'dom', exec: nodeFromDataToString }, { item: 'converter', from: 'boolean', to: 'dom', exec: nodeFromDataToString }, { item: 'converter', from: 'undefined', to: 'dom', exec: function(data, conversionContext) { return util.createElement(conversionContext.document, 'span'); } }, { item: 'converter', from: 'error', to: 'dom', exec: function(ex, conversionContext) { var node = util.createElement(conversionContext.document, 'p'); node.className = 'gcli-error'; node.textContent = ex; return node; } }, { item: 'converter', from: 'error', to: 'string', exec: function(ex, conversionContext) { return '' + ex; } } ]; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/converters/terminal', ['require', 'exports', 'module' , 'util/util'], function(require, exports, module) { 'use strict'; var util = require('util/util'); /** * A 'terminal' object is a string or an array of strings, which are typically * the output from a shell command */ exports.items = [ { item: 'converter', from: 'terminal', to: 'dom', createTextArea: function(text, conversionContext) { var node = util.createElement(conversionContext.document, 'textarea'); node.classList.add('gcli-row-subterminal'); node.readOnly = true; node.textContent = text; return node; }, exec: function(data, conversionContext) { if (Array.isArray(data)) { var node = util.createElement(conversionContext.document, 'div'); data.forEach(function(member) { node.appendChild(this.createTextArea(member, conversionContext)); }); return node; } return this.createTextArea(data); } }, { item: 'converter', from: 'terminal', to: 'string', exec: function(data, conversionContext) { return Array.isArray(data) ? data.join('') : '' + data; } } ]; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/ui/intro', ['require', 'exports', 'module' , 'util/l10n', 'gcli/settings', 'gcli/ui/view', 'gcli/cli', 'text!gcli/ui/intro.html'], function(require, exports, module) { 'use strict'; var l10n = require('util/l10n'); var settings = require('gcli/settings'); var view = require('gcli/ui/view'); var Output = require('gcli/cli').Output; /** * Record if the user has clicked on 'Got It!' */ exports.items = [ { item: 'setting', name: 'hideIntro', type: 'boolean', description: l10n.lookup('hideIntroDesc'), defaultValue: false } ]; /** * Called when the UI is ready to add a welcome message to the output */ exports.maybeShowIntro = function(commandOutputManager, conversionContext) { var hideIntro = settings.getSetting('hideIntro'); if (hideIntro.value) { return; } var output = new Output(); output.type = 'view'; commandOutputManager.onOutput({ output: output }); var viewData = this.createView(null, conversionContext, output); output.complete({ isTypedData: true, type: 'view', data: viewData }); }; /** * Called when the UI is ready to add a welcome message to the output */ exports.createView = function(ignore, conversionContext, output) { return view.createView({ html: require('text!gcli/ui/intro.html'), options: { stack: 'intro.html' }, data: { l10n: l10n.propertyLookup, onclick: conversionContext.update, ondblclick: conversionContext.updateExec, showHideButton: (output != null), onGotIt: function(ev) { var hideIntro = settings.getSetting('hideIntro'); hideIntro.value = true; output.onClose(); } } }); }; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/ui/view', ['require', 'exports', 'module' , 'util/util', 'util/domtemplate'], function(require, exports, module) { 'use strict'; var util = require('util/util'); var domtemplate = require('util/domtemplate'); /** * We want to avoid commands having to create DOM structures because that's * messy and because we're going to need to have command output displayed in * different documents. A View is a way to wrap an HTML template (for * domtemplate) in with the data and options to render the template, so anyone * can later run the template in the context of any document. * View also cuts out a chunk of boiler place code. * @param options The information needed to create the DOM from HTML. Includes: * - html (required): The HTML source, probably from a call to require * - options (default={}): The domtemplate options. See domtemplate for details * - data (default={}): The data to domtemplate. See domtemplate for details. * - css (default=none): Some CSS to be added to the final document. If 'css' * is used, use of cssId is strongly recommended. * - cssId (default=none): An ID to prevent multiple CSS additions. See * util.importCss for more details. * @return An object containing a single function 'appendTo()' which runs the * template adding the result to the specified element. Takes 2 parameters: * - element (required): the element to add to * - clear (default=false): if clear===true then remove all pre-existing * children of 'element' before appending the results of this template. */ exports.createView = function(options) { if (options.html == null) { throw new Error('options.html is missing'); } return { /** * RTTI. Yeah. */ isView: true, /** * Run the template against the document to which element belongs. * @param element The element to append the result to * @param clear Set clear===true to remove all children of element */ appendTo: function(element, clear) { // Strict check on the off-chance that we later think of other options // and want to replace 'clear' with an 'options' parameter, but want to // support backwards compat. if (clear === true) { util.clearElement(element); } element.appendChild(this.toDom(element.ownerDocument)); }, /** * Actually convert the view data into a DOM suitable to be appended to * an element * @param document to use in realizing the template */ toDom: function(document) { if (options.css) { util.importCss(options.css, document, options.cssId); } var child = util.toDom(document, options.html); domtemplate.template(child, options.data || {}, options.options || {}); return child; } }; }; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('util/domtemplate', ['require', 'exports', 'module' ], function(require, exports, module) { 'use strict'; var obj = {}; Components.utils.import('resource://gre/modules/devtools/Templater.jsm', obj); exports.template = obj.template; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/cli', ['require', 'exports', 'module' , 'util/promise', 'util/util', 'util/l10n', 'gcli/ui/view', 'gcli/converters', 'gcli/canon', 'gcli/types', 'gcli/argument'], function(require, exports, module) { 'use strict'; var promise = require('util/promise'); var util = require('util/util'); var l10n = require('util/l10n'); var view = require('gcli/ui/view'); var converters = require('gcli/converters'); var canon = require('gcli/canon'); var CommandOutputManager = require('gcli/canon').CommandOutputManager; var Status = require('gcli/types').Status; var Conversion = require('gcli/types').Conversion; var Argument = require('gcli/argument').Argument; var ArrayArgument = require('gcli/argument').ArrayArgument; var NamedArgument = require('gcli/argument').NamedArgument; var TrueNamedArgument = require('gcli/argument').TrueNamedArgument; var MergedArgument = require('gcli/argument').MergedArgument; var ScriptArgument = require('gcli/argument').ScriptArgument; /** * Some manual intervention is needed in parsing the { command. */ function getEvalCommand() { if (getEvalCommand._cmd == null) { getEvalCommand._cmd = canon.getCommand(evalCmd.name); } return getEvalCommand._cmd; } /** * Assignment is a link between a parameter and the data for that parameter. * The data for the parameter is available as in the preferred type and as * an Argument for the CLI. *

We also record validity information where applicable. *

For values, null and undefined have distinct definitions. null means * that a value has been provided, undefined means that it has not. * Thus, null is a valid default value, and common because it identifies an * parameter that is optional. undefined means there is no value from * the command line. * @constructor */ function Assignment(param, paramIndex) { // The parameter that we are assigning to this.param = param; this.conversion = undefined; // The index of this parameter in the parent Requisition. paramIndex === -1 // is the command assignment although this should not be relied upon, it is // better to test param instanceof CommandAssignment this.paramIndex = paramIndex; } /** * Easy accessor for conversion.arg. * This is a read-only property because writes to arg should be done through * the 'conversion' property. */ Object.defineProperty(Assignment.prototype, 'arg', { get: function() { return this.conversion == null ? undefined : this.conversion.arg; }, enumerable: true }); /** * Easy accessor for conversion.value. * This is a read-only property because writes to value should be done through * the 'conversion' property. */ Object.defineProperty(Assignment.prototype, 'value', { get: function() { return this.conversion == null ? undefined : this.conversion.value; }, enumerable: true }); /** * Easy (and safe) accessor for conversion.message */ Object.defineProperty(Assignment.prototype, 'message', { get: function() { return this.conversion == null || !this.conversion.message ? '' : this.conversion.message; }, enumerable: true }); /** * Easy (and safe) accessor for conversion.getPredictions() * @return An array of objects with name and value elements. For example: * [ { name:'bestmatch', value:foo1 }, { name:'next', value:foo2 }, ... ] */ Assignment.prototype.getPredictions = function() { return this.conversion == null ? [] : this.conversion.getPredictions(); }; /** * Accessor for a prediction by index. * This is useful above getPredictions()[index] because it normalizes * index to be within the bounds of the predictions, which means that the UI * can maintain an index of which prediction to choose without caring how many * predictions there are. * @param index The index of the prediction to choose */ Assignment.prototype.getPredictionAt = function(index) { if (index == null) { index = 0; } if (this.isInName()) { return promise.resolve(undefined); } return this.getPredictions().then(function(predictions) { if (predictions.length === 0) { return undefined; } index = index % predictions.length; if (index < 0) { index = predictions.length + index; } return predictions[index]; }.bind(this)); }; /** * Some places want to take special action if we are in the name part of a * named argument (i.e. the '--foo' bit). * Currently this does not take actual cursor position into account, it just * assumes that the cursor is at the end. In the future we will probably want * to take this into account. */ Assignment.prototype.isInName = function() { return this.conversion.arg.type === 'NamedArgument' && this.conversion.arg.prefix.slice(-1) !== ' '; }; /** * Work out what the status of the current conversion is which involves looking * not only at the conversion, but also checking if data has been provided * where it should. * @param arg For assignments with multiple args (e.g. array assignments) we * can narrow the search for status to a single argument. */ Assignment.prototype.getStatus = function(arg) { if (this.param.isDataRequired && !this.conversion.isDataProvided()) { return Status.INCOMPLETE; } // Selection/Boolean types with a defined range of values will say that // '' is INCOMPLETE, but the parameter may be optional, so we don't ask // if the user doesn't need to enter something and hasn't done so. if (!this.param.isDataRequired && this.arg.type === 'BlankArgument') { return Status.VALID; } return this.conversion.getStatus(arg); }; /** * Helper when we're rebuilding command lines. */ Assignment.prototype.toString = function() { return this.conversion.toString(); }; /** * For test/debug use only. The output from this function is subject to wanton * random change without notice, and should not be relied upon to even exist * at some later date. */ Object.defineProperty(Assignment.prototype, '_summaryJson', { get: function() { var predictionCount = ''; this.getPredictions().then(function(predictions) { predictionCount = predictions.length; }, console.log); return { param: this.param.name + '/' + this.param.type.name, defaultValue: this.param.defaultValue, arg: this.conversion.arg._summaryJson, value: this.value, message: this.message, status: this.getStatus().toString(), predictionCount: predictionCount }; }, enumerable: true }); exports.Assignment = Assignment; /** * How to dynamically execute JavaScript code */ var customEval = eval; /** * Setup a function to be called in place of 'eval', generally for security * reasons */ exports.setEvalFunction = function(newCustomEval) { customEval = newCustomEval; }; /** * Remove the binding done by setEvalFunction(). * We purposely set customEval to undefined rather than to 'eval' because there * is an implication of setEvalFunction that we're in a security sensitive * situation. What if we can trick GCLI into calling unsetEvalFunction() at the * wrong time? * So to properly undo the effects of setEvalFunction(), you need to call * setEvalFunction(eval) rather than unsetEvalFunction(), however the latter is * preferred in most cases. */ exports.unsetEvalFunction = function() { customEval = undefined; }; /** * 'eval' command */ var evalCmd = { item: 'command', name: '{', params: [ { name: 'javascript', type: 'javascript', description: '' } ], hidden: true, returnType: 'object', description: { key: 'cliEvalJavascript' }, exec: function(args, context) { return customEval(args.javascript); }, isCommandRegexp: /^\s*{\s*/ }; exports.items = [ evalCmd ]; /** * This is a special assignment to reflect the command itself. */ function CommandAssignment() { var commandParamMetadata = { name: '__command', type: { name: 'command', allowNonExec: false } }; // This is a hack so that rather than reply with a generic description of the // command assignment, we reply with the description of the assigned command, // (using a generic term if there is no assigned command) var self = this; Object.defineProperty(commandParamMetadata, 'description', { get: function() { var value = self.value; return value && value.description ? value.description : 'The command to execute'; }, enumerable: true }); this.param = new canon.Parameter(commandParamMetadata); this.paramIndex = -1; } CommandAssignment.prototype = Object.create(Assignment.prototype); CommandAssignment.prototype.getStatus = function(arg) { return Status.combine( Assignment.prototype.getStatus.call(this, arg), this.conversion.value && this.conversion.value.exec ? Status.VALID : Status.INCOMPLETE ); }; exports.CommandAssignment = CommandAssignment; /** * Special assignment used when ignoring parameters that don't have a home */ function UnassignedAssignment(requisition, arg) { this.param = new canon.Parameter({ name: '__unassigned', description: l10n.lookup('cliOptions'), type: { name: 'param', requisition: requisition, isIncompleteName: (arg.text.charAt(0) === '-') } }); this.paramIndex = -1; // synchronize is ok because we can be sure that param type is synchronous var parsed = this.param.type.parse(arg, requisition.executionContext); this.conversion = util.synchronize(parsed); this.conversion.assignment = this; } UnassignedAssignment.prototype = Object.create(Assignment.prototype); UnassignedAssignment.prototype.getStatus = function(arg) { return this.conversion.getStatus(); }; exports.logErrors = true; /** * A Requisition collects the information needed to execute a command. * * (For a definition of the term, see http://en.wikipedia.org/wiki/Requisition) * This term is used because carries the notion of a work-flow, or process to * getting the information to execute a command correct. * There is little point in a requisition for parameter-less commands because * there is no information to collect. A Requisition is a collection of * assignments of values to parameters, each handled by an instance of * Assignment. * *

Events

*

Requisition publishes the following events: *

    *
  • onTextChange: The text to be mirrored in a command line has changed. *
* * @param environment An optional opaque object passed to commands in the * Execution Context. * @param doc A DOM Document passed to commands using the Execution Context in * order to allow creation of DOM nodes. If missing Requisition will use the * global 'document'. * @param commandOutputManager A custom commandOutputManager to which output * should be sent (optional) * @constructor */ function Requisition(environment, doc, commandOutputManager) { this.environment = environment; this.document = doc; if (this.document == null) { try { this.document = document; } catch (ex) { // Ignore } } this.commandOutputManager = commandOutputManager || new CommandOutputManager(); this.shell = { cwd: '/', // Where we store the current working directory env: {} // Where we store the current environment }; this.onTextChange = util.createEvent('Requisition.onTextChange'); // The command that we are about to execute. // @see setCommandConversion() this.commandAssignment = new CommandAssignment(); var assignPromise = this.setAssignment(this.commandAssignment, null, { internal: true }); util.synchronize(assignPromise); // The object that stores of Assignment objects that we are filling out. // The Assignment objects are stored under their param.name for named // lookup. Note: We make use of the property of Javascript objects that // they are not just hashmaps, but linked-list hashmaps which iterate in // insertion order. // _assignments excludes the commandAssignment. this._assignments = {}; // The count of assignments. Excludes the commandAssignment this.assignmentCount = 0; // Used to store cli arguments in the order entered on the cli this._args = []; // Used to store cli arguments that were not assigned to parameters this._unassigned = []; // Changes can be asynchronous, when one update starts before another // finishes we abandon the former change this._nextUpdateId = 0; // We can set a prefix to typed commands to make it easier to focus on // Allowing us to type "add -a; commit" in place of "git add -a; git commit" this.prefix = ''; } /** * Avoid memory leaks */ Requisition.prototype.destroy = function() { delete this.document; delete this.environment; }; /** * If we're about to make an asynchronous change when other async changes could * overtake this one, then we want to be able to bail out if overtaken. The * value passed back from beginChange should be passed to endChangeCheckOrder * on completion of calculation, before the results are applied in order to * check that the calculation has not been overtaken */ Requisition.prototype._beginChange = function() { this.onTextChange.holdFire(); var updateId = this._nextUpdateId; this._nextUpdateId++; return updateId; }; /** * Check to see if another change has started since updateId started. * This allows us to bail out of an update. * It's hard to make updates atomic because until you've responded to a parse * of the command argument, you don't know how to parse the arguments to that * command. */ Requisition.prototype._isChangeCurrent = function(updateId) { return updateId + 1 === this._nextUpdateId; }; /** * See notes on beginChange */ Requisition.prototype._endChangeCheckOrder = function(updateId) { this.onTextChange.resumeFire(); if (updateId + 1 !== this._nextUpdateId) { // An update that started after we did has already finished, so our // changes are out of date. Abandon further work. return false; } return true; }; var legacy = false; /** * Functions and data related to the execution of a command */ Object.defineProperty(Requisition.prototype, 'executionContext', { get: function() { if (this._executionContext == null) { this._executionContext = { defer: function() { return promise.defer(); }, typedData: function(type, data) { return { isTypedData: true, data: data, type: type }; }, getArgsObject: this.getArgsObject.bind(this) }; // Alias requisition so we're clear about what's what var requisition = this; Object.defineProperty(this._executionContext, 'typed', { get: function() { return requisition.toString(); }, enumerable: true }); Object.defineProperty(this._executionContext, 'environment', { get: function() { return requisition.environment; }, enumerable: true }); Object.defineProperty(this._executionContext, 'shell', { get: function() { return requisition.shell; }, enumerable : true }); /** * This is a temporary property that will change and/or be removed. * Do not use it */ Object.defineProperty(this._executionContext, '__dlhjshfw', { get: function() { return requisition; }, enumerable: false }); if (legacy) { this._executionContext.createView = view.createView; this._executionContext.exec = this.exec.bind(this); this._executionContext.update = this.update.bind(this); this._executionContext.updateExec = this.updateExec.bind(this); Object.defineProperty(this._executionContext, 'document', { get: function() { return requisition.document; }, enumerable: true }); } } return this._executionContext; }, enumerable: true }); /** * Functions and data related to the conversion of the output of a command */ Object.defineProperty(Requisition.prototype, 'conversionContext', { get: function() { if (this._conversionContext == null) { this._conversionContext = { defer: function() { return promise.defer(); }, createView: view.createView, exec: this.exec.bind(this), update: this.update.bind(this), updateExec: this.updateExec.bind(this) }; // Alias requisition so we're clear about what's what var requisition = this; Object.defineProperty(this._conversionContext, 'document', { get: function() { return requisition.document; }, enumerable: true }); Object.defineProperty(this._conversionContext, 'environment', { get: function() { return requisition.environment; }, enumerable: true }); /** * This is a temporary property that will change and/or be removed. * Do not use it */ Object.defineProperty(this._conversionContext, '__dlhjshfw', { get: function() { return requisition; }, enumerable: false }); } return this._conversionContext; }, enumerable: true }); /** * Assignments have an order, so we need to store them in an array. * But we also need named access ... * @return The found assignment, or undefined, if no match was found */ Requisition.prototype.getAssignment = function(nameOrNumber) { var name = (typeof nameOrNumber === 'string') ? nameOrNumber : Object.keys(this._assignments)[nameOrNumber]; return this._assignments[name] || undefined; }; /** * There are a few places where we need to know what the 'next thing' is. What * is the user going to be filling out next (assuming they don't enter a named * argument). The next argument is the first in line that is both blank, and * that can be filled in positionally. * @return The next assignment to be used, or null if all the positional * parameters have values. */ Requisition.prototype._getFirstBlankPositionalAssignment = function() { var reply = null; Object.keys(this._assignments).some(function(name) { var assignment = this.getAssignment(name); if (assignment.arg.type === 'BlankArgument' && assignment.param.isPositionalAllowed) { reply = assignment; return true; // i.e. break } return false; }, this); return reply; }; /** * Where parameter name == assignment names - they are the same */ Requisition.prototype.getParameterNames = function() { return Object.keys(this._assignments); }; /** * A *shallow* clone of the assignments. * This is useful for systems that wish to go over all the assignments * finding values one way or another and wish to trim an array as they go. */ Requisition.prototype.cloneAssignments = function() { return Object.keys(this._assignments).map(function(name) { return this._assignments[name]; }, this); }; /** * The overall status is the most severe status. * There is no such thing as an INCOMPLETE overall status because the * definition of INCOMPLETE takes into account the cursor position to say 'this * isn't quite ERROR because the user can fix it by typing', however overall, * this is still an error status. */ Object.defineProperty(Requisition.prototype, 'status', { get : function() { var status = Status.VALID; if (this._unassigned.length !== 0) { var isAllIncomplete = true; this._unassigned.forEach(function(assignment) { if (!assignment.param.type.isIncompleteName) { isAllIncomplete = false; } }); status = isAllIncomplete ? Status.INCOMPLETE : Status.ERROR; } this.getAssignments(true).forEach(function(assignment) { var assignStatus = assignment.getStatus(); if (assignStatus > status) { status = assignStatus; } }, this); if (status === Status.INCOMPLETE) { status = Status.ERROR; } return status; }, enumerable : true }); /** * If ``requisition.status != VALID`` message then return a string which * best describes what is wrong. Generally error messages are delivered by * looking at the error associated with the argument at the cursor, but there * are times when you just want to say 'tell me the worst'. * If ``requisition.status != VALID`` then return ``null``. */ Requisition.prototype.getStatusMessage = function() { if (this.commandAssignment.getStatus() !== Status.VALID) { return l10n.lookup('cliUnknownCommand'); } var assignments = this.getAssignments(); for (var i = 0; i < assignments.length; i++) { if (assignments[i].getStatus() !== Status.VALID) { return assignments[i].message; } } if (this._unassigned.length !== 0) { return l10n.lookup('cliUnusedArg'); } return null; }; /** * Extract the names and values of all the assignments, and return as * an object. */ Requisition.prototype.getArgsObject = function() { var args = {}; this.getAssignments().forEach(function(assignment) { args[assignment.param.name] = assignment.conversion.isDataProvided() ? assignment.value : assignment.param.defaultValue; }, this); return args; }; /** * Access the arguments as an array. * @param includeCommand By default only the parameter arguments are * returned unless (includeCommand === true), in which case the list is * prepended with commandAssignment.arg */ Requisition.prototype.getAssignments = function(includeCommand) { var assignments = []; if (includeCommand === true) { assignments.push(this.commandAssignment); } Object.keys(this._assignments).forEach(function(name) { assignments.push(this.getAssignment(name)); }, this); return assignments; }; /** * When any assignment changes, we might need to update the _args array to * match and inform people of changes to the typed input text. */ Requisition.prototype._setAssignmentInternal = function(assignment, conversion) { var oldConversion = assignment.conversion; assignment.conversion = conversion; assignment.conversion.assignment = assignment; // Do nothing if the conversion is unchanged if (assignment.conversion.equals(oldConversion)) { if (assignment === this.commandAssignment) { this.setBlankArguments(); } return; } // When the command changes, we need to keep a bunch of stuff in sync if (assignment === this.commandAssignment) { this._assignments = {}; var command = this.commandAssignment.value; if (command) { for (var i = 0; i < command.params.length; i++) { var param = command.params[i]; var newAssignment = new Assignment(param, i); var assignPromise = this.setAssignment(newAssignment, null, { internal: true }); util.synchronize(assignPromise); this._assignments[param.name] = newAssignment; } } this.assignmentCount = Object.keys(this._assignments).length; } // For the onTextChange event, we only care about changes to the argument if (!assignment.conversion.argEquals(oldConversion)) { this.onTextChange(); } }; /** * Internal function to alter the given assignment using the given arg. * @param assignment The assignment to alter * @param arg The new value for the assignment. An instance of Argument, or an * instance of Conversion, or null to set the blank value. * @param options There are a number of ways to customize how the assignment * is made, including: * - internal: (default:false) External updates are required to do more work, * including adjusting the args in this requisition to stay in sync. * On the other hand non internal changes use beginChange to back out of * changes when overtaken asynchronously. * Setting internal:true effectively means this is being called as part of * the update process. * - matchPadding: (default:false) Alter the whitespace on the prefix and * suffix of the new argument to match that of the old argument. This only * makes sense with internal=false */ Requisition.prototype.setAssignment = function(assignment, arg, options) { options = options || {}; if (!options.internal) { var originalArgs = assignment.arg.getArgs(); // Update the args array var replacementArgs = arg.getArgs(); var maxLen = Math.max(originalArgs.length, replacementArgs.length); for (var i = 0; i < maxLen; i++) { // If there are no more original args, or if the original arg was blank // (i.e. not typed by the user), we'll just need to add at the end if (i >= originalArgs.length || originalArgs[i].type === 'BlankArgument') { this._args.push(replacementArgs[i]); continue; } var index = this._args.indexOf(originalArgs[i]); if (index === -1) { console.error('Couldn\'t find ', originalArgs[i], ' in ', this._args); throw new Error('Couldn\'t find ' + originalArgs[i]); } // If there are no more replacement args, we just remove the original args // Otherwise swap original args and replacements if (i >= replacementArgs.length) { this._args.splice(index, 1); } else { if (options.matchPadding) { if (replacementArgs[i].prefix.length === 0 && this._args[index].prefix.length !== 0) { replacementArgs[i].prefix = this._args[index].prefix; } if (replacementArgs[i].suffix.length === 0 && this._args[index].suffix.length !== 0) { replacementArgs[i].suffix = this._args[index].suffix; } } this._args[index] = replacementArgs[i]; } } } var updateId = options.internal ? null : this._beginChange(); var setAssignmentInternal = function(conversion) { if (options.internal || this._endChangeCheckOrder(updateId)) { this._setAssignmentInternal(assignment, conversion); } return promise.resolve(undefined); }.bind(this); if (arg == null) { var blank = assignment.param.type.getBlank(this.executionContext); return setAssignmentInternal(blank); } if (typeof arg.getStatus === 'function') { // It's not really an arg, it's a conversion already return setAssignmentInternal(arg); } var parsed = assignment.param.type.parse(arg, this.executionContext); return parsed.then(setAssignmentInternal); }; /** * Reset all the assignments to their default values */ Requisition.prototype.setBlankArguments = function() { this.getAssignments().forEach(function(assignment) { var assignPromise = this.setAssignment(assignment, null, { internal: true }); util.synchronize(assignPromise); }, this); }; /** * Complete the argument at cursor. * Basically the same as: * assignment = getAssignmentAt(cursor); * assignment.value = assignment.conversion.predictions[0]; * Except it's done safely, and with particular care to where we place the * space, which is complex, and annoying if we get it wrong. * * WARNING: complete() can happen asynchronously. * * @param cursor The cursor configuration. Should have start and end properties * which should be set to start and end of the selection. * @param predictionChoice The index of the prediction that we should choose. * This number is not bounded by the size of the prediction array, we take the * modulus to get it within bounds * @return A promise which completes (with undefined) when any outstanding * completion tasks are done. */ Requisition.prototype.complete = function(cursor, predictionChoice) { var assignment = this.getAssignmentAt(cursor.start); var predictionPromise = assignment.getPredictionAt(predictionChoice); return predictionPromise.then(function(prediction) { var outstanding = []; this.onTextChange.holdFire(); // Note: Since complete is asynchronous we should perhaps have a system to // bail out of making changes if the command line has changed since TAB // was pressed. It's not yet clear if this will be a problem. if (prediction == null) { // No predictions generally means we shouldn't change anything on TAB, // but TAB has the connotation of 'next thing' and when we're at the end // of a thing that implies that we should add a space. i.e. // 'help' -> 'help ' // But we should only do this if the thing that we're 'completing' is // valid and doesn't already end in a space. if (assignment.arg.suffix.slice(-1) !== ' ' && assignment.getStatus() === Status.VALID) { outstanding.push(this._addSpace(assignment)); } // Also add a space if we are in the name part of an assignment, however // this time we don't want the 'push the space to the next assignment' // logic, so we don't use addSpace if (assignment.isInName()) { var newArg = assignment.arg.beget({ prefixPostSpace: true }); outstanding.push(this.setAssignment(assignment, newArg)); } } else { // Mutate this argument to hold the completion var arg = assignment.arg.beget({ text: prediction.name, dontQuote: (assignment === this.commandAssignment) }); var assignPromise = this.setAssignment(assignment, arg); if (!prediction.incomplete) { assignPromise = assignPromise.then(function() { // The prediction is complete, add a space to let the user move-on return this._addSpace(assignment).then(function() { // Bug 779443 - Remove or explain the re-parse if (assignment instanceof UnassignedAssignment) { return this.update(this.toString()); } }.bind(this)); }.bind(this)); } outstanding.push(assignPromise); } return promise.all(outstanding).then(function() { this.onTextChange(); this.onTextChange.resumeFire(); return true; }.bind(this)); }.bind(this)); }; /** * A test method to check that all args are assigned in some way */ Requisition.prototype._assertArgsAssigned = function() { this._args.forEach(function(arg) { if (arg.assignment == null) { console.log('No assignment for ' + arg); } }, this); }; /** * Pressing TAB sometimes requires that we add a space to denote that we're on * to the 'next thing'. * @param assignment The assignment to which to append the space */ Requisition.prototype._addSpace = function(assignment) { var arg = assignment.arg.beget({ suffixSpace: true }); if (arg !== assignment.arg) { return this.setAssignment(assignment, arg); } else { return promise.resolve(undefined); } }; /** * Replace the current value with the lower value if such a concept exists. */ Requisition.prototype.decrement = function(assignment) { var replacement = assignment.param.type.decrement(assignment.value, this.executionContext); if (replacement != null) { var str = assignment.param.type.stringify(replacement, this.executionContext); var arg = assignment.arg.beget({ text: str }); var assignPromise = this.setAssignment(assignment, arg); util.synchronize(assignPromise); } }; /** * Replace the current value with the higher value if such a concept exists. */ Requisition.prototype.increment = function(assignment) { var replacement = assignment.param.type.increment(assignment.value, this.executionContext); if (replacement != null) { var str = assignment.param.type.stringify(replacement, this.executionContext); var arg = assignment.arg.beget({ text: str }); var assignPromise = this.setAssignment(assignment, arg); util.synchronize(assignPromise); } }; /** * Extract a canonical version of the input */ Requisition.prototype.toCanonicalString = function() { var line = []; var cmd = this.commandAssignment.value ? this.commandAssignment.value.name : this.commandAssignment.arg.text; line.push(cmd); Object.keys(this._assignments).forEach(function(name) { var assignment = this._assignments[name]; var type = assignment.param.type; // Bug 664377: This will cause problems if there is a non-default value // after a default value. Also we need to decide when to use // named parameters in place of positional params. Both can wait. if (assignment.value !== assignment.param.defaultValue) { line.push(' '); line.push(type.stringify(assignment.value, this.executionContext)); } }, this); // Canonically, if we've opened with a { then we should have a } to close if (cmd === '{') { if (this.getAssignment(0).arg.suffix.indexOf('}') === -1) { line.push(' }'); } } return line.join(''); }; /** * Input trace gives us an array of Argument tracing objects, one for each * character in the typed input, from which we can derive information about how * to display this typed input. It's a bit like toString on steroids. *

* The returned object has the following members:

    *
  • character: The character to which this arg trace refers. *
  • arg: The Argument to which this character is assigned. *
  • part: One of ['prefix'|'text'|suffix'] - how was this char understood *
*

* The Argument objects are as output from tokenize() rather than as applied * to Assignments by _assign() (i.e. they are not instances of NamedArgument, * ArrayArgument, etc). *

* To get at the arguments applied to the assignments simply call * arg.assignment.arg. If arg.assignment.arg !== arg then * the arg applied to the assignment will contain the original arg. * See _assign() for details. */ Requisition.prototype.createInputArgTrace = function() { if (!this._args) { throw new Error('createInputMap requires a command line. See source.'); // If this is a problem then we can fake command line input using // something like the code in #toCanonicalString(). } var args = []; var i; this._args.forEach(function(arg) { for (i = 0; i < arg.prefix.length; i++) { args.push({ arg: arg, character: arg.prefix[i], part: 'prefix' }); } for (i = 0; i < arg.text.length; i++) { args.push({ arg: arg, character: arg.text[i], part: 'text' }); } for (i = 0; i < arg.suffix.length; i++) { args.push({ arg: arg, character: arg.suffix[i], part: 'suffix' }); } }); return args; }; /** * Reconstitute the input from the args */ Requisition.prototype.toString = function() { if (this._args) { return this._args.map(function(arg) { return arg.toString(); }).join(''); } return this.toCanonicalString(); }; /** * If the last character is whitespace then things that we suggest to add to * the end don't need a space prefix. * While this is quite a niche function, it has 2 benefits: * - it's more correct because we can distinguish between final whitespace that * is part of an unclosed string, and parameter separating whitespace. * - also it's faster than toString() the whole thing and checking the end char * @return true iff the last character is interpreted as parameter separating * whitespace */ Requisition.prototype.typedEndsWithSeparator = function() { // This is not as easy as doing (this.toString().slice(-1) === ' ') // See the doc comments above; We're checking for separators, not spaces if (this._args) { var lastArg = this._args.slice(-1)[0]; if (lastArg.suffix.slice(-1) === ' ') { return true; } return lastArg.text === '' && lastArg.suffix === '' && lastArg.prefix.slice(-1) === ' '; } return this.toCanonicalString().slice(-1) === ' '; }; /** * Return an array of Status scores so we can create a marked up * version of the command line input. * @param cursor We only take a status of INCOMPLETE to be INCOMPLETE when the * cursor is actually in the argument. Otherwise it's an error. * @return Array of objects each containing status property and a * string property containing the characters to which the status * applies. Concatenating the strings in order gives the original input. */ Requisition.prototype.getInputStatusMarkup = function(cursor) { var argTraces = this.createInputArgTrace(); // Generally the 'argument at the cursor' is the argument before the cursor // unless it is before the first char, in which case we take the first. cursor = cursor === 0 ? 0 : cursor - 1; var cTrace = argTraces[cursor]; var markup = []; for (var i = 0; i < argTraces.length; i++) { var argTrace = argTraces[i]; var arg = argTrace.arg; var status = Status.VALID; if (argTrace.part === 'text') { status = arg.assignment.getStatus(arg); // Promote INCOMPLETE to ERROR ... if (status === Status.INCOMPLETE) { // If the cursor is in the prefix or suffix of an argument then we // don't consider it in the argument for the purposes of preventing // the escalation to ERROR. However if this is a NamedArgument, then we // allow the suffix (as space between 2 parts of the argument) to be in. // We use arg.assignment.arg not arg because we're looking at the arg // that got put into the assignment not as returned by tokenize() var isNamed = (cTrace.arg.assignment.arg.type === 'NamedArgument'); var isInside = cTrace.part === 'text' || (isNamed && cTrace.part === 'suffix'); if (arg.assignment !== cTrace.arg.assignment || !isInside) { // And if we're not in the command if (!(arg.assignment instanceof CommandAssignment)) { status = Status.ERROR; } } } } markup.push({ status: status, string: argTrace.character }); } // De-dupe: merge entries where 2 adjacent have same status i = 0; while (i < markup.length - 1) { if (markup[i].status === markup[i + 1].status) { markup[i].string += markup[i + 1].string; markup.splice(i + 1, 1); } else { i++; } } return markup; }; /** * Look through the arguments attached to our assignments for the assignment * at the given position. * @param {number} cursor The cursor position to query */ Requisition.prototype.getAssignmentAt = function(cursor) { if (!this._args) { console.trace(); throw new Error('Missing args'); } // We short circuit this one because we may have no args, or no args with // any size and the alg below only finds arguments with size. if (cursor === 0) { return this.commandAssignment; } var assignForPos = []; var i, j; for (i = 0; i < this._args.length; i++) { var arg = this._args[i]; var assignment = arg.assignment; // prefix and text are clearly part of the argument for (j = 0; j < arg.prefix.length; j++) { assignForPos.push(assignment); } for (j = 0; j < arg.text.length; j++) { assignForPos.push(assignment); } // suffix is part of the argument only if this is a named parameter, // otherwise it looks forwards if (arg.assignment.arg.type === 'NamedArgument') { // leave the argument as it is } else if (this._args.length > i + 1) { // first to the next argument assignment = this._args[i + 1].assignment; } else { // then to the first blank positional parameter, leaving 'as is' if none var nextAssignment = this._getFirstBlankPositionalAssignment(); if (nextAssignment != null) { assignment = nextAssignment; } } for (j = 0; j < arg.suffix.length; j++) { assignForPos.push(assignment); } } // Possible shortcut, we don't really need to go through all the args // to work out the solution to this var reply = assignForPos[cursor - 1]; if (!reply) { throw new Error('Missing assignment.' + ' cursor=' + cursor + ' text=' + this.toString()); } return reply; }; /** * Entry point for keyboard accelerators or anything else that wants to execute * a command. * @param options Object describing how the execution should be handled. * (optional). Contains some of the following properties: * - hidden (boolean, default=false) Should the output be hidden from the * commandOutputManager for this requisition * - command/args A fast shortcut to executing a known command with a known * set of parsed arguments. */ Requisition.prototype.exec = function(options) { var command = null; var args = null; var hidden = false; if (options) { if (options.hidden) { hidden = true; } if (options.command != null) { // Fast track by looking up the command directly since passed args // means there is no command line to parse. command = canon.getCommand(options.command); if (!command) { console.error('Command not found: ' + options.command); } args = options.args; } } if (!command) { command = this.commandAssignment.value; args = this.getArgsObject(); } // Display JavaScript input without the initial { or closing } var typed = this.toString(); if (evalCmd.isCommandRegexp.test(typed)) { typed = typed.replace(evalCmd.isCommandRegexp, ''); // Bug 717763: What if the JavaScript naturally ends with a }? typed = typed.replace(/\s*}\s*$/, ''); } var output = new Output({ command: command, args: args, typed: typed, canonical: this.toCanonicalString(), hidden: hidden }); this.commandOutputManager.onOutput({ output: output }); var onDone = function(data) { output.complete(data, false); return output; }; var onError = function(ex) { if (exports.logErrors) { util.errorHandler(ex); } var data = ex.isTypedData ? ex : { isTypedData: true, data: ex, type: 'error' }; output.complete(data, true); return output; }; if (this.status !== Status.VALID) { var ex = new Error(this.getStatusMessage()); return promise.resolve(onError(ex)).then(function(output) { this.clear(); return output; }); } else { try { var reply = command.exec(args, this.executionContext); return promise.resolve(reply).then(onDone, onError); } catch (ex) { return promise.resolve(onError(ex)); } finally { this.clear(); } } }; /** * A shortcut for calling update, resolving the promise and then exec. * @param input The string to execute * @param options Passed to exec * @return A promise of an output object */ Requisition.prototype.updateExec = function(input, options) { return this.update(input).then(function() { return this.exec(options); }.bind(this)); }; /** * Similar to update('') except that it's guaranteed to execute synchronously */ Requisition.prototype.clear = function() { this.onTextChange.holdFire(); var arg = new Argument('', '', ''); this._args = [ arg ]; var commandType = this.commandAssignment.param.type; var parsePromise = commandType.parse(arg, this.executionContext); this.setAssignment(this.commandAssignment, util.synchronize(parsePromise), { internal: true }); this.onTextChange.resumeFire(); this.onTextChange(); }; /** * Helper to find the 'data-command' attribute, used by |update()| */ function getDataCommandAttribute(element) { var command = element.getAttribute('data-command'); if (!command) { command = element.querySelector('*[data-command]') .getAttribute('data-command'); } return command; } /** * Called by the UI when ever the user interacts with a command line input * @param typed The contents of the input field */ Requisition.prototype.update = function(typed) { if (typeof HTMLElement !== 'undefined' && typed instanceof HTMLElement) { typed = getDataCommandAttribute(typed); } if (typeof Event !== 'undefined' && typed instanceof Event) { typed = getDataCommandAttribute(typed.currentTarget); } var updateId = this._beginChange(); this._args = exports.tokenize(typed); var args = this._args.slice(0); // i.e. clone return this._split(args).then(function() { if (!this._isChangeCurrent(updateId)) { return false; } return this._assign(args).then(function() { if (this._endChangeCheckOrder(updateId)) { this.onTextChange(); return true; } return false; }.bind(this)); }.bind(this)); }; /** * For test/debug use only. The output from this function is subject to wanton * random change without notice, and should not be relied upon to even exist * at some later date. */ Object.defineProperty(Requisition.prototype, '_summaryJson', { get: function() { var summary = { $args: this._args.map(function(arg) { return arg._summaryJson; }), _command: this.commandAssignment._summaryJson, _unassigned: this._unassigned.forEach(function(assignment) { return assignment._summaryJson; }) }; Object.keys(this._assignments).forEach(function(name) { summary[name] = this.getAssignment(name)._summaryJson; }.bind(this)); return summary; }, enumerable: true }); /** * tokenize() is a state machine. These are the states. */ var In = { /** * The last character was ' '. * Typing a ' ' character will not change the mode * Typing one of '"{ will change mode to SINGLE_Q, DOUBLE_Q or SCRIPT. * Anything else goes into SIMPLE mode. */ WHITESPACE: 1, /** * The last character was part of a parameter. * Typing ' ' returns to WHITESPACE mode. Any other character * (including '"{} which are otherwise special) does not change the mode. */ SIMPLE: 2, /** * We're inside single quotes: ' * Typing ' returns to WHITESPACE mode. Other characters do not change mode. */ SINGLE_Q: 3, /** * We're inside double quotes: " * Typing " returns to WHITESPACE mode. Other characters do not change mode. */ DOUBLE_Q: 4, /** * We're inside { and } * Typing } returns to WHITESPACE mode. Other characters do not change mode. * SCRIPT mode is slightly different from other modes in that spaces between * the {/} delimiters and the actual input are not considered significant. * e.g: " x " is a 3 character string, delimited by double quotes, however * { x } is a 1 character JavaScript surrounded by whitespace and {} * delimiters. * In the short term we assume that the JS routines can make sense of the * extra whitespace, however at some stage we may need to move the space into * the Argument prefix/suffix. * Also we don't attempt to handle nested {}. See bug 678961 */ SCRIPT: 5 }; /** * Split up the input taking into account ', " and {. * We don't consider \t or other classical whitespace characters to split * arguments apart. For one thing these characters are hard to type, but also * if the user has gone to the trouble of pasting a TAB character into the * input field (or whatever it takes), they probably mean it. */ exports.tokenize = function(typed) { // For blank input, place a dummy empty argument into the list if (typed == null || typed.length === 0) { return [ new Argument('', '', '') ]; } if (isSimple(typed)) { return [ new Argument(typed, '', '') ]; } var mode = In.WHITESPACE; // First we swap out escaped characters that are special to the tokenizer. // So a backslash followed by any of ['"{} ] is turned into a unicode private // char so we can swap back later typed = typed .replace(/\\\\/g, '\uF000') .replace(/\\ /g, '\uF001') .replace(/\\'/g, '\uF002') .replace(/\\"/g, '\uF003') .replace(/\\{/g, '\uF004') .replace(/\\}/g, '\uF005'); function unescape2(escaped) { return escaped .replace(/\uF000/g, '\\\\') .replace(/\uF001/g, '\\ ') .replace(/\uF002/g, '\\\'') .replace(/\uF003/g, '\\\"') .replace(/\uF004/g, '\\{') .replace(/\uF005/g, '\\}'); } var i = 0; // The index of the current character var start = 0; // Where did this section start? var prefix = ''; // Stuff that comes before the current argument var args = []; // The array that we're creating var blockDepth = 0; // For JS with nested {} // This is just a state machine. We're going through the string char by char // The 'mode' is one of the 'In' states. As we go, we're adding Arguments // to the 'args' array. while (true) { var c = typed[i]; var str; switch (mode) { case In.WHITESPACE: if (c === '\'') { prefix = typed.substring(start, i + 1); mode = In.SINGLE_Q; start = i + 1; } else if (c === '"') { prefix = typed.substring(start, i + 1); mode = In.DOUBLE_Q; start = i + 1; } else if (c === '{') { prefix = typed.substring(start, i + 1); mode = In.SCRIPT; blockDepth++; start = i + 1; } else if (/ /.test(c)) { // Still whitespace, do nothing } else { prefix = typed.substring(start, i); mode = In.SIMPLE; start = i; } break; case In.SIMPLE: // There is an edge case of xx'xx which we are assuming to // be a single parameter (and same with ") if (c === ' ') { str = unescape2(typed.substring(start, i)); args.push(new Argument(str, prefix, '')); mode = In.WHITESPACE; start = i; prefix = ''; } break; case In.SINGLE_Q: if (c === '\'') { str = unescape2(typed.substring(start, i)); args.push(new Argument(str, prefix, c)); mode = In.WHITESPACE; start = i + 1; prefix = ''; } break; case In.DOUBLE_Q: if (c === '"') { str = unescape2(typed.substring(start, i)); args.push(new Argument(str, prefix, c)); mode = In.WHITESPACE; start = i + 1; prefix = ''; } break; case In.SCRIPT: if (c === '{') { blockDepth++; } else if (c === '}') { blockDepth--; if (blockDepth === 0) { str = unescape2(typed.substring(start, i)); args.push(new ScriptArgument(str, prefix, c)); mode = In.WHITESPACE; start = i + 1; prefix = ''; } } break; } i++; if (i >= typed.length) { // There is nothing else to read - tidy up if (mode === In.WHITESPACE) { if (i !== start) { // There's whitespace at the end of the typed string. Add it to the // last argument's suffix, creating an empty argument if needed. var extra = typed.substring(start, i); var lastArg = args[args.length - 1]; if (!lastArg) { args.push(new Argument('', extra, '')); } else { lastArg.suffix += extra; } } } else if (mode === In.SCRIPT) { str = unescape2(typed.substring(start, i + 1)); args.push(new ScriptArgument(str, prefix, '')); } else { str = unescape2(typed.substring(start, i + 1)); args.push(new Argument(str, prefix, '')); } break; } } return args; }; /** * If the input has no spaces, quotes, braces or escapes, * we can take the fast track. */ function isSimple(typed) { for (var i = 0; i < typed.length; i++) { var c = typed.charAt(i); if (c === ' ' || c === '"' || c === '\'' || c === '{' || c === '}' || c === '\\') { return false; } } return true; } /** * Looks in the canon for a command extension that matches what has been * typed at the command line. */ Requisition.prototype._split = function(args) { // We're processing args, so we don't want the assignments that we make to // try to adjust other args assuming this is an external update var noArgUp = { internal: true }; // Handle the special case of the user typing { javascript(); } // We use the hidden 'eval' command directly rather than shift()ing one of // the parameters, and parse()ing it. var conversion; if (args[0].type === 'ScriptArgument') { // Special case: if the user enters { console.log('foo'); } then we need to // use the hidden 'eval' command conversion = new Conversion(getEvalCommand(), new ScriptArgument()); return this.setAssignment(this.commandAssignment, conversion, noArgUp); } var argsUsed = 1; var parsePromise; var commandType = this.commandAssignment.param.type; while (argsUsed <= args.length) { var arg = (argsUsed === 1) ? args[0] : new MergedArgument(args, 0, argsUsed); // Making the commandType.parse() promise as synchronous is OK because we // know that commandType is a synchronous type. if (this.prefix != null && this.prefix !== '') { var prefixArg = new Argument(this.prefix, '', ' '); var prefixedArg = new MergedArgument([ prefixArg, arg ]); parsePromise = commandType.parse(prefixedArg, this.executionContext); conversion = util.synchronize(parsePromise); if (conversion.value == null) { parsePromise = commandType.parse(arg, this.executionContext); conversion = util.synchronize(parsePromise); } } else { parsePromise = commandType.parse(arg, this.executionContext); conversion = util.synchronize(parsePromise); } // We only want to carry on if this command is a parent command, // which means that there is a commandAssignment, but not one with // an exec function. if (!conversion.value || conversion.value.exec) { break; } // Previously we needed a way to hide commands depending context. // We have not resurrected that feature yet, but if we do we should // insert code here to ignore certain commands depending on the // context/environment argsUsed++; } // This could probably be re-written to consume args as we go for (var i = 0; i < argsUsed; i++) { args.shift(); } // Warning: we're returning a promise (from setAssignment) which tells us // when we're done setting the current command, but mutating the args array // as we go, so we're conflicted on when we're done return this.setAssignment(this.commandAssignment, conversion, noArgUp); }; /** * Add all the passed args to the list of unassigned assignments. */ Requisition.prototype._addUnassignedArgs = function(args) { args.forEach(function(arg) { this._unassigned.push(new UnassignedAssignment(this, arg)); }.bind(this)); }; /** * Work out which arguments are applicable to which parameters. */ Requisition.prototype._assign = function(args) { // See comment in _split. Avoid multiple updates var noArgUp = { internal: true }; this._unassigned = []; var outstanding = []; if (!this.commandAssignment.value) { this._addUnassignedArgs(args); return promise.all(outstanding); } if (args.length === 0) { this.setBlankArguments(); return promise.all(outstanding); } // Create an error if the command does not take parameters, but we have // been given them ... if (this.assignmentCount === 0) { this._addUnassignedArgs(args); return promise.all(outstanding); } // Special case: if there is only 1 parameter, and that's of type // text, then we put all the params into the first param if (this.assignmentCount === 1) { var assignment = this.getAssignment(0); if (assignment.param.type.name === 'string') { var arg = (args.length === 1) ? args[0] : new MergedArgument(args); outstanding.push(this.setAssignment(assignment, arg, noArgUp)); return promise.all(outstanding); } } // Positional arguments can still be specified by name, but if they are // then we need to ignore them when working them out positionally var unassignedParams = this.getParameterNames(); // We collect the arguments used in arrays here before assigning var arrayArgs = {}; // Extract all the named parameters this.getAssignments(false).forEach(function(assignment) { // Loop over the arguments // Using while rather than loop because we remove args as we go var i = 0; while (i < args.length) { if (assignment.param.isKnownAs(args[i].text)) { var arg = args.splice(i, 1)[0]; unassignedParams = unassignedParams.filter(function(test) { return test !== assignment.param.name; }); // boolean parameters don't have values, default to false if (assignment.param.type.name === 'boolean') { arg = new TrueNamedArgument(arg); } else { var valueArg = null; if (i + 1 <= args.length) { valueArg = args.splice(i, 1)[0]; } arg = new NamedArgument(arg, valueArg); } if (assignment.param.type.name === 'array') { var arrayArg = arrayArgs[assignment.param.name]; if (!arrayArg) { arrayArg = new ArrayArgument(); arrayArgs[assignment.param.name] = arrayArg; } arrayArg.addArgument(arg); } else { if (assignment.arg.type === 'BlankArgument') { outstanding.push(this.setAssignment(assignment, arg, noArgUp)); } else { this._addUnassignedArgs(arg.getArgs()); } } } else { // Skip this parameter and handle as a positional parameter i++; } } }, this); // What's left are positional parameters: assign in order unassignedParams.forEach(function(name) { var assignment = this.getAssignment(name); // If not set positionally, and we can't set it non-positionally, // we have to default it to prevent previous values surviving if (!assignment.param.isPositionalAllowed) { outstanding.push(this.setAssignment(assignment, null, noArgUp)); return; } // If this is a positional array argument, then it swallows the // rest of the arguments. if (assignment.param.type.name === 'array') { var arrayArg = arrayArgs[assignment.param.name]; if (!arrayArg) { arrayArg = new ArrayArgument(); arrayArgs[assignment.param.name] = arrayArg; } arrayArg.addArguments(args); args = []; // The actual assignment to the array parameter is done below return; } // Set assignment to defaults if there are no more arguments if (args.length === 0) { outstanding.push(this.setAssignment(assignment, null, noArgUp)); return; } var arg = args.splice(0, 1)[0]; // --foo and -f are named parameters, -4 is a number. So '-' is either // the start of a named parameter or a number depending on the context var isIncompleteName = assignment.param.type.name === 'number' ? /-[-a-zA-Z_]/.test(arg.text) : arg.text.charAt(0) === '-'; if (isIncompleteName) { this._unassigned.push(new UnassignedAssignment(this, arg)); } else { outstanding.push(this.setAssignment(assignment, arg, noArgUp)); } }, this); // Now we need to assign the array argument (if any) Object.keys(arrayArgs).forEach(function(name) { var assignment = this.getAssignment(name); outstanding.push(this.setAssignment(assignment, arrayArgs[name], noArgUp)); }, this); // What's left is can't be assigned, but we need to officially unassign them this._addUnassignedArgs(args); return promise.all(outstanding); }; exports.Requisition = Requisition; /** * A simple object to hold information about the output of a command */ function Output(options) { options = options || {}; this.command = options.command || ''; this.args = options.args || {}; this.typed = options.typed || ''; this.canonical = options.canonical || ''; this.hidden = options.hidden === true ? true : false; this.type = undefined; this.data = undefined; this.completed = false; this.error = false; this.start = new Date(); this._deferred = promise.defer(); this.promise = this._deferred.promise; this.onClose = util.createEvent('Output.onClose'); } /** * Called when there is data to display, and the command has finished executing * See changed() for details on parameters. */ Output.prototype.complete = function(data, error) { this.end = new Date(); this.duration = this.end.getTime() - this.start.getTime(); this.completed = true; this.error = error; if (data != null && data.isTypedData) { this.data = data.data; this.type = data.type; } else { this.data = data; this.type = this.command.returnType; if (this.type == null) { this.type = (this.data == null) ? 'undefined' : typeof this.data; } } if (this.type === 'object') { throw new Error('No type from output of ' + this.typed); } this._deferred.resolve(); }; /** * Call converters.convert using the data in this Output object */ Output.prototype.convert = function(type, conversionContext) { return converters.convert(this.data, this.type, type, conversionContext); }; exports.Output = Output; }); define("text!gcli/ui/intro.html", [], "\n" + "

\n" + "

${l10n.introTextOpening2}

\n" + "\n" + "

\n" + " ${l10n.introTextCommands}\n" + " help${l10n.introTextKeys2}\n" + " ${l10n.introTextF1Escape}.\n" + "

\n" + "\n" + " \n" + "
\n" + ""); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/ui/focus', ['require', 'exports', 'module' , 'util/util', 'util/l10n', 'gcli/settings'], function(require, exports, module) { 'use strict'; var util = require('util/util'); var l10n = require('util/l10n'); var settings = require('gcli/settings'); /** * Record how much help the user wants from the tooltip */ var Eagerness = { NEVER: 1, SOMETIMES: 2, ALWAYS: 3 }; /** * Export the eagerHelper setting */ exports.items = [ { item: 'setting', name: 'eagerHelper', type: { name: 'selection', lookup: [ { name: 'never', value: Eagerness.NEVER }, { name: 'sometimes', value: Eagerness.SOMETIMES }, { name: 'always', value: Eagerness.ALWAYS } ] }, defaultValue: Eagerness.SOMETIMES, description: l10n.lookup('eagerHelperDesc'), ignoreTypeDifference: true } ]; /** * FocusManager solves the problem of tracking focus among a set of nodes. * The specific problem we are solving is when the hint element must be visible * if either the command line or any of the inputs in the hint element has the * focus, and invisible at other times, without hiding and showing the hint * element even briefly as the focus changes between them. * It does this simply by postponing the hide events by 250ms to see if * something else takes focus. * @param options Object containing user customization properties, including: * - blurDelay (default=150ms) * - debug (default=false) * @param components Object that links to other UI components. GCLI provided: * - document * - requisition */ function FocusManager(options, components) { options = options || {}; this._document = components.document || document; this._requisition = components.requisition; this._debug = options.debug || false; this._blurDelay = options.blurDelay || 150; this._window = this._document.defaultView; this._requisition.commandOutputManager.onOutput.add(this._outputted, this); this._blurDelayTimeout = null; // Result of setTimeout in delaying a blur this._monitoredElements = []; // See addMonitoredElement() this._isError = false; this._hasFocus = false; this._helpRequested = false; this._recentOutput = false; this.onVisibilityChange = util.createEvent('FocusManager.onVisibilityChange'); this._focused = this._focused.bind(this); this._document.addEventListener('focus', this._focused, true); var eagerHelper = settings.getSetting('eagerHelper'); eagerHelper.onChange.add(this._eagerHelperChanged, this); this.isTooltipVisible = undefined; this.isOutputVisible = undefined; this._checkShow(); } /** * Avoid memory leaks */ FocusManager.prototype.destroy = function() { var eagerHelper = settings.getSetting('eagerHelper'); eagerHelper.onChange.remove(this._eagerHelperChanged, this); this._document.removeEventListener('focus', this._focused, true); this._requisition.commandOutputManager.onOutput.remove(this._outputted, this); for (var i = 0; i < this._monitoredElements.length; i++) { var monitor = this._monitoredElements[i]; console.error('Hanging monitored element: ', monitor.element); monitor.element.removeEventListener('focus', monitor.onFocus, true); monitor.element.removeEventListener('blur', monitor.onBlur, true); } if (this._blurDelayTimeout) { this._window.clearTimeout(this._blurDelayTimeout); this._blurDelayTimeout = null; } delete this._focused; delete this._document; delete this._window; delete this._requisition; }; /** * The easy way to include an element in the set of things that are part of the * aggregate focus. Using [add|remove]MonitoredElement() is a simpler way of * option than calling report[Focus|Blur]() * @param element The element on which to track focus|blur events * @param where Optional source string for debugging only */ FocusManager.prototype.addMonitoredElement = function(element, where) { if (this._debug) { console.log('FocusManager.addMonitoredElement(' + (where || 'unknown') + ')'); } var monitor = { element: element, where: where, onFocus: function() { this._reportFocus(where); }.bind(this), onBlur: function() { this._reportBlur(where); }.bind(this) }; element.addEventListener('focus', monitor.onFocus, true); element.addEventListener('blur', monitor.onBlur, true); if (this._document.activeElement === element) { this._reportFocus(where); } this._monitoredElements.push(monitor); }; /** * Undo the effects of addMonitoredElement() * @param element The element to stop tracking * @param where Optional source string for debugging only */ FocusManager.prototype.removeMonitoredElement = function(element, where) { if (this._debug) { console.log('FocusManager.removeMonitoredElement(' + (where || 'unknown') + ')'); } var newMonitoredElements = this._monitoredElements.filter(function(monitor) { if (monitor.element === element) { element.removeEventListener('focus', monitor.onFocus, true); element.removeEventListener('blur', monitor.onBlur, true); return false; } return true; }); this._monitoredElements = newMonitoredElements; }; /** * Monitor for new command executions */ FocusManager.prototype.updatePosition = function(dimensions) { var ev = { tooltipVisible: this.isTooltipVisible, outputVisible: this.isOutputVisible, dimensions: dimensions }; this.onVisibilityChange(ev); }; /** * Monitor for new command executions */ FocusManager.prototype._outputted = function(ev) { this._recentOutput = true; this._helpRequested = false; this._checkShow(); }; /** * We take a focus event anywhere to be an indication that we might be about * to lose focus */ FocusManager.prototype._focused = function() { this._reportBlur('document'); }; /** * Some component has received a 'focus' event. This sets the internal status * straight away and informs the listeners * @param where Optional source string for debugging only */ FocusManager.prototype._reportFocus = function(where) { if (this._debug) { console.log('FocusManager._reportFocus(' + (where || 'unknown') + ')'); } if (this._blurDelayTimeout) { if (this._debug) { console.log('FocusManager.cancelBlur'); } this._window.clearTimeout(this._blurDelayTimeout); this._blurDelayTimeout = null; } if (!this._hasFocus) { this._hasFocus = true; } this._checkShow(); }; /** * Some component has received a 'blur' event. This waits for a while to see if * we are going to get any subsequent 'focus' events and then sets the internal * status and informs the listeners * @param where Optional source string for debugging only */ FocusManager.prototype._reportBlur = function(where) { if (this._debug) { console.log('FocusManager._reportBlur(' + where + ')'); } if (this._hasFocus) { if (this._blurDelayTimeout) { if (this._debug) { console.log('FocusManager.blurPending'); } return; } this._blurDelayTimeout = this._window.setTimeout(function() { if (this._debug) { console.log('FocusManager.blur'); } this._hasFocus = false; this._checkShow(); this._blurDelayTimeout = null; }.bind(this), this._blurDelay); } }; /** * The setting has changed */ FocusManager.prototype._eagerHelperChanged = function() { this._checkShow(); }; /** * The inputter tells us about keyboard events so we can decide to delay * showing the tooltip element */ FocusManager.prototype.onInputChange = function() { this._recentOutput = false; this._checkShow(); }; /** * Generally called for something like a F1 key press, when the user explicitly * wants help */ FocusManager.prototype.helpRequest = function() { if (this._debug) { console.log('FocusManager.helpRequest'); } this._helpRequested = true; this._recentOutput = false; this._checkShow(); }; /** * Generally called for something like a ESC key press, when the user explicitly * wants to get rid of the help */ FocusManager.prototype.removeHelp = function() { if (this._debug) { console.log('FocusManager.removeHelp'); } this._importantFieldFlag = false; this._isError = false; this._helpRequested = false; this._recentOutput = false; this._checkShow(); }; /** * Set to true whenever a field thinks it's output is important */ FocusManager.prototype.setImportantFieldFlag = function(flag) { if (this._debug) { console.log('FocusManager.setImportantFieldFlag', flag); } this._importantFieldFlag = flag; this._checkShow(); }; /** * Set to true whenever a field thinks it's output is important */ FocusManager.prototype.setError = function(isError) { if (this._debug) { console.log('FocusManager._isError', isError); } this._isError = isError; this._checkShow(); }; /** * Helper to compare the current showing state with the value calculated by * _shouldShow() and take appropriate action */ FocusManager.prototype._checkShow = function() { var fire = false; var ev = { tooltipVisible: this.isTooltipVisible, outputVisible: this.isOutputVisible }; var showTooltip = this._shouldShowTooltip(); if (this.isTooltipVisible !== showTooltip.visible) { ev.tooltipVisible = this.isTooltipVisible = showTooltip.visible; fire = true; } var showOutput = this._shouldShowOutput(); if (this.isOutputVisible !== showOutput.visible) { ev.outputVisible = this.isOutputVisible = showOutput.visible; fire = true; } if (fire) { if (this._debug) { console.log('FocusManager.onVisibilityChange', ev); } this.onVisibilityChange(ev); } }; /** * Calculate if we should be showing or hidden taking into account all the * available inputs */ FocusManager.prototype._shouldShowTooltip = function() { if (!this._hasFocus) { return { visible: false, reason: 'notHasFocus' }; } var eagerHelper = settings.getSetting('eagerHelper'); if (eagerHelper.value === Eagerness.NEVER) { return { visible: false, reason: 'eagerHelperNever' }; } if (eagerHelper.value === Eagerness.ALWAYS) { return { visible: true, reason: 'eagerHelperAlways' }; } if (this._isError) { return { visible: true, reason: 'isError' }; } if (this._helpRequested) { return { visible: true, reason: 'helpRequested' }; } if (this._importantFieldFlag) { return { visible: true, reason: 'importantFieldFlag' }; } return { visible: false, reason: 'default' }; }; /** * Calculate if we should be showing or hidden taking into account all the * available inputs */ FocusManager.prototype._shouldShowOutput = function() { if (!this._hasFocus) { return { visible: false, reason: 'notHasFocus' }; } if (this._recentOutput) { return { visible: true, reason: 'recentOutput' }; } return { visible: false, reason: 'default' }; }; exports.FocusManager = FocusManager; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/ui/fields/basic', ['require', 'exports', 'module' , 'util/util', 'util/promise', 'util/l10n', 'gcli/argument', 'gcli/types', 'gcli/ui/fields'], function(require, exports, module) { 'use strict'; var util = require('util/util'); var promise = require('util/promise'); var l10n = require('util/l10n'); var Argument = require('gcli/argument').Argument; var TrueNamedArgument = require('gcli/argument').TrueNamedArgument; var FalseNamedArgument = require('gcli/argument').FalseNamedArgument; var ArrayArgument = require('gcli/argument').ArrayArgument; var ArrayConversion = require('gcli/types').ArrayConversion; var Field = require('gcli/ui/fields').Field; var fields = require('gcli/ui/fields'); /** * A field that allows editing of strings */ function StringField(type, options) { Field.call(this, type, options); this.arg = new Argument(); this.element = util.createElement(this.document, 'input'); this.element.type = 'text'; this.element.classList.add('gcli-field'); this.onInputChange = this.onInputChange.bind(this); this.element.addEventListener('keyup', this.onInputChange, false); this.onFieldChange = util.createEvent('StringField.onFieldChange'); } StringField.prototype = Object.create(Field.prototype); StringField.prototype.destroy = function() { Field.prototype.destroy.call(this); this.element.removeEventListener('keyup', this.onInputChange, false); delete this.element; delete this.document; delete this.onInputChange; }; StringField.prototype.setConversion = function(conversion) { this.arg = conversion.arg; this.element.value = conversion.arg.text; this.setMessage(conversion.message); }; StringField.prototype.getConversion = function() { // This tweaks the prefix/suffix of the argument to fit this.arg = this.arg.beget({ text: this.element.value, prefixSpace: true }); return this.type.parse(this.arg, this.requisition.executionContext); }; StringField.claim = function(type, context) { return type.name === 'string' ? Field.MATCH : Field.BASIC; }; /** * A field that allows editing of numbers using an [input type=number] field */ function NumberField(type, options) { Field.call(this, type, options); this.arg = new Argument(); this.element = util.createElement(this.document, 'input'); this.element.type = 'number'; if (this.type.max) { this.element.max = this.type.max; } if (this.type.min) { this.element.min = this.type.min; } if (this.type.step) { this.element.step = this.type.step; } this.onInputChange = this.onInputChange.bind(this); this.element.addEventListener('keyup', this.onInputChange, false); this.onFieldChange = util.createEvent('NumberField.onFieldChange'); } NumberField.prototype = Object.create(Field.prototype); NumberField.claim = function(type, context) { return type.name === 'number' ? Field.MATCH : Field.NO_MATCH; }; NumberField.prototype.destroy = function() { Field.prototype.destroy.call(this); this.element.removeEventListener('keyup', this.onInputChange, false); delete this.element; delete this.document; delete this.onInputChange; }; NumberField.prototype.setConversion = function(conversion) { this.arg = conversion.arg; this.element.value = conversion.arg.text; this.setMessage(conversion.message); }; NumberField.prototype.getConversion = function() { this.arg = this.arg.beget({ text: this.element.value, prefixSpace: true }); return this.type.parse(this.arg, this.requisition.executionContext); }; /** * A field that uses a checkbox to toggle a boolean field */ function BooleanField(type, options) { Field.call(this, type, options); this.name = options.name; this.named = options.named; this.element = util.createElement(this.document, 'input'); this.element.type = 'checkbox'; this.element.id = 'gcliForm' + this.name; this.onInputChange = this.onInputChange.bind(this); this.element.addEventListener('change', this.onInputChange, false); this.onFieldChange = util.createEvent('BooleanField.onFieldChange'); } BooleanField.prototype = Object.create(Field.prototype); BooleanField.claim = function(type, context) { return type.name === 'boolean' ? Field.MATCH : Field.NO_MATCH; }; BooleanField.prototype.destroy = function() { Field.prototype.destroy.call(this); this.element.removeEventListener('change', this.onInputChange, false); delete this.element; delete this.document; delete this.onInputChange; }; BooleanField.prototype.setConversion = function(conversion) { this.element.checked = conversion.value; this.setMessage(conversion.message); }; BooleanField.prototype.getConversion = function() { var arg; if (this.named) { arg = this.element.checked ? new TrueNamedArgument(new Argument(' --' + this.name)) : new FalseNamedArgument(); } else { arg = new Argument(' ' + this.element.checked); } return this.type.parse(arg, this.requisition.executionContext); }; /** * A field that works with delegate types by delaying resolution until that * last possible time */ function DelegateField(type, options) { Field.call(this, type, options); this.options = options; this.requisition.onTextChange.add(this.update, this); this.element = util.createElement(this.document, 'div'); this.update(); this.onFieldChange = util.createEvent('DelegateField.onFieldChange'); } DelegateField.prototype = Object.create(Field.prototype); DelegateField.prototype.update = function() { var subtype = this.type.delegateType(); if (subtype === this.subtype) { return; } if (this.field) { this.field.onFieldChange.remove(this.fieldChanged, this); this.field.destroy(); } this.subtype = subtype; this.field = fields.getField(subtype, this.options); this.field.onFieldChange.add(this.fieldChanged, this); util.clearElement(this.element); this.element.appendChild(this.field.element); }; DelegateField.claim = function(type, context) { return type.isDelegate ? Field.MATCH : Field.NO_MATCH; }; DelegateField.prototype.destroy = function() { Field.prototype.destroy.call(this); this.requisition.onTextChange.remove(this.update, this); delete this.element; delete this.document; delete this.onInputChange; }; DelegateField.prototype.setConversion = function(conversion) { this.field.setConversion(conversion); }; DelegateField.prototype.getConversion = function() { return this.field.getConversion(); }; Object.defineProperty(DelegateField.prototype, 'isImportant', { get: function() { return this.field.isImportant; }, enumerable: true }); /** * Adds add/delete buttons to a normal field allowing there to be many values * given for a parameter. */ function ArrayField(type, options) { Field.call(this, type, options); this.options = options; this._onAdd = this._onAdd.bind(this); this.members = []; //
this.element = util.createElement(this.document, 'div'); this.element.classList.add('gcli-array-parent'); // \n" + "
\n" + ""); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/ui/ffdisplay', ['require', 'exports', 'module' , 'gcli/ui/inputter', 'gcli/ui/completer', 'gcli/ui/tooltip', 'gcli/ui/focus', 'gcli/cli', 'gcli/types/javascript', 'gcli/types/node', 'gcli/types/resource', 'util/host', 'gcli/ui/intro', 'gcli/canon'], function(require, exports, module) { 'use strict'; var Inputter = require('gcli/ui/inputter').Inputter; var Completer = require('gcli/ui/completer').Completer; var Tooltip = require('gcli/ui/tooltip').Tooltip; var FocusManager = require('gcli/ui/focus').FocusManager; var Requisition = require('gcli/cli').Requisition; var cli = require('gcli/cli'); var jstype = require('gcli/types/javascript'); var nodetype = require('gcli/types/node'); var resource = require('gcli/types/resource'); var host = require('util/host'); var intro = require('gcli/ui/intro'); var CommandOutputManager = require('gcli/canon').CommandOutputManager; /** * Handy utility to inject the content document (i.e. for the viewed page, * not for chrome) into the various components. */ function setContentDocument(document) { if (document) { nodetype.setDocument(document); resource.setDocument(document); } else { resource.unsetDocument(); nodetype.unsetDocument(); jstype.unsetGlobalObject(); } } /** * FFDisplay is responsible for generating the UI for GCLI, this implementation * is a special case for use inside Firefox * @param options A configuration object containing the following: * - contentDocument (optional) * - chromeDocument * - hintElement * - inputElement * - completeElement * - backgroundElement * - outputDocument * - consoleWrap (optional) * - eval (optional) * - environment * - scratchpad (optional) * - chromeWindow * - commandOutputManager (optional) */ function FFDisplay(options) { if (options.eval) { cli.setEvalFunction(options.eval); } setContentDocument(options.contentDocument); host.chromeWindow = options.chromeWindow; this.commandOutputManager = options.commandOutputManager; if (this.commandOutputManager == null) { this.commandOutputManager = new CommandOutputManager(); } this.onOutput = this.commandOutputManager.onOutput; this.requisition = new Requisition(options.environment, options.outputDocument, this.commandOutputManager); this.focusManager = new FocusManager(options, { document: options.chromeDocument, requisition: this.requisition, }); this.onVisibilityChange = this.focusManager.onVisibilityChange; this.inputter = new Inputter(options, { requisition: this.requisition, focusManager: this.focusManager, element: options.inputElement }); this.completer = new Completer(options, { requisition: this.requisition, inputter: this.inputter, backgroundElement: options.backgroundElement, element: options.completeElement }); this.tooltip = new Tooltip(options, { requisition: this.requisition, focusManager: this.focusManager, inputter: this.inputter, element: options.hintElement }); this.inputter.tooltip = this.tooltip; if (options.consoleWrap) { this.consoleWrap = options.consoleWrap; var win = options.consoleWrap.ownerDocument.defaultView; this.resizer = this.resizer.bind(this); win.addEventListener('resize', this.resizer, false); this.requisition.onTextChange.add(this.resizer, this); } this.options = options; } /** * The main Display calls this as part of startup since it registers listeners * for output first. The firefox display can't do this, so it has to be a * separate method */ FFDisplay.prototype.maybeShowIntro = function() { intro.maybeShowIntro(this.commandOutputManager, this.requisition.conversionContext); }; /** * Called when the page to which we're attached changes * @params options Object with the following properties: * - contentDocument: Points to the page that we should now work against * - environment: A replacement environment for Requisition use * - chromeWindow: Allow node type to create overlay */ FFDisplay.prototype.reattach = function(options) { setContentDocument(options.contentDocument); host.chromeWindow = options.chromeWindow; this.requisition.environment = options.environment; }; /** * Avoid memory leaks */ FFDisplay.prototype.destroy = function() { if (this.consoleWrap) { var win = this.options.consoleWrap.ownerDocument.defaultView; this.requisition.onTextChange.remove(this.resizer, this); win.removeEventListener('resize', this.resizer, false); } this.tooltip.destroy(); this.completer.destroy(); this.inputter.destroy(); this.focusManager.destroy(); this.requisition.destroy(); host.chromeWindow = undefined; setContentDocument(null); cli.unsetEvalFunction(); delete this.options; // We could also delete the following objects if we have hard-to-track-down // memory leaks, as a belt-and-braces approach, however this prevents our // DOM node hunter script from looking in all the nooks and crannies, so it's // better if we can be leak-free without deleting them: // - consoleWrap, resizer, tooltip, completer, inputter, // - focusManager, onVisibilityChange, requisition, commandOutputManager }; /** * Called on chrome window resize, or on divider slide */ FFDisplay.prototype.resizer = function() { // Bug 705109: There are several numbers hard-coded in this function. // This is simpler than calculating them, but error-prone when the UI setup, // the styling or display settings change. var parentRect = this.options.consoleWrap.getBoundingClientRect(); // Magic number: 64 is the size of the toolbar above the output area var parentHeight = parentRect.bottom - parentRect.top - 64; // Magic number: 100 is the size at which we decide the hints are too small // to be useful, so we hide them if (parentHeight < 100) { this.options.hintElement.classList.add('gcliterm-hint-nospace'); } else { this.options.hintElement.classList.remove('gcliterm-hint-nospace'); this.options.hintElement.style.overflowY = null; this.options.hintElement.style.borderBottomColor = 'white'; } // We also try to make the max-width of any GCLI elements so they don't // extend outside the scroll area. var doc = this.options.hintElement.ownerDocument; var outputNode = this.options.hintElement.parentNode.parentNode.children[1]; var outputs = outputNode.getElementsByClassName('gcliterm-msg-body'); var listItems = outputNode.getElementsByClassName('hud-msg-node'); // This is an top-side estimate. We could try to calculate it, maybe using // something along these lines http://www.alexandre-gomes.com/?p=115 However // experience has shown this to be hard to get to work reliably // Also we don't need to be precise. If we use a number that is too big then // the only down-side is too great a right margin var scrollbarWidth = 20; if (listItems.length > 0) { var parentWidth = outputNode.getBoundingClientRect().width - scrollbarWidth; var otherWidth; var body; for (var i = 0; i < listItems.length; ++i) { var listItem = listItems[i]; // a.k.a 'var otherWidth = 132' otherWidth = 0; body = null; for (var j = 0; j < listItem.children.length; j++) { var child = listItem.children[j]; if (child.classList.contains('gcliterm-msg-body')) { body = child.children[0]; } else { otherWidth += child.getBoundingClientRect().width; } var styles = doc.defaultView.getComputedStyle(child, null); otherWidth += parseInt(styles.borderLeftWidth, 10) + parseInt(styles.borderRightWidth, 10) + parseInt(styles.paddingLeft, 10) + parseInt(styles.paddingRight, 10) + parseInt(styles.marginLeft, 10) + parseInt(styles.marginRight, 10); } if (body) { body.style.width = (parentWidth - otherWidth) + 'px'; } } } }; exports.FFDisplay = FFDisplay; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/ui/inputter', ['require', 'exports', 'module' , 'util/promise', 'util/util', 'gcli/types', 'gcli/history', 'text!gcli/ui/inputter.css'], function(require, exports, module) { 'use strict'; var promise = require('util/promise'); var util = require('util/util'); var KeyEvent = require('util/util').KeyEvent; var Status = require('gcli/types').Status; var History = require('gcli/history').History; var inputterCss = require('text!gcli/ui/inputter.css'); var RESOLVED = promise.resolve(true); /** * A wrapper to take care of the functions concerning an input element * @param options Object containing user customization properties, including: * - scratchpad (default=none) * - promptWidth (default=22px) * @param components Object that links to other UI components. GCLI provided: * - requisition * - focusManager * - element */ function Inputter(options, components) { this.requisition = components.requisition; this.focusManager = components.focusManager; this.element = components.element; this.element.classList.add('gcli-in-input'); this.element.spellcheck = false; this.document = this.element.ownerDocument; this.scratchpad = options.scratchpad; if (inputterCss != null) { this.style = util.importCss(inputterCss, this.document, 'gcli-inputter'); } // Used to distinguish focus from TAB in CLI. See onKeyUp() this.lastTabDownAt = 0; // Used to effect caret changes. See _processCaretChange() this._caretChange = null; // Ensure that TAB/UP/DOWN isn't handled by the browser this.onKeyDown = this.onKeyDown.bind(this); this.onKeyUp = this.onKeyUp.bind(this); this.element.addEventListener('keydown', this.onKeyDown, false); this.element.addEventListener('keyup', this.onKeyUp, false); // Setup History this.history = new History(); this._scrollingThroughHistory = false; // Used when we're selecting which prediction to complete with this._choice = null; this.onChoiceChange = util.createEvent('Inputter.onChoiceChange'); // Cursor position affects hint severity this.onMouseUp = this.onMouseUp.bind(this); this.element.addEventListener('mouseup', this.onMouseUp, false); if (this.focusManager) { this.focusManager.addMonitoredElement(this.element, 'input'); } // Initially an asynchronous completion isn't in-progress this._completed = RESOLVED; this.requisition.onTextChange.add(this.textChanged, this); this.assignment = this.requisition.getAssignmentAt(0); this.onAssignmentChange = util.createEvent('Inputter.onAssignmentChange'); this.onInputChange = util.createEvent('Inputter.onInputChange'); this.onResize = util.createEvent('Inputter.onResize'); this.onWindowResize = this.onWindowResize.bind(this); this.document.defaultView.addEventListener('resize', this.onWindowResize, false); this._previousValue = undefined; this.requisition.update(this.element.value || ''); } /** * Avoid memory leaks */ Inputter.prototype.destroy = function() { this.document.defaultView.removeEventListener('resize', this.onWindowResize, false); this.requisition.onTextChange.remove(this.textChanged, this); if (this.focusManager) { this.focusManager.removeMonitoredElement(this.element, 'input'); } this.element.removeEventListener('mouseup', this.onMouseUp, false); this.element.removeEventListener('keydown', this.onKeyDown, false); this.element.removeEventListener('keyup', this.onKeyUp, false); this.history.destroy(); if (this.style) { this.style.parentNode.removeChild(this.style); delete this.style; } delete this.onMouseUp; delete this.onKeyDown; delete this.onKeyUp; delete this.onWindowResize; delete this.tooltip; delete this.document; delete this.element; }; /** * Make ourselves visually similar to the input element, and make the input * element transparent so our background shines through */ Inputter.prototype.onWindowResize = function() { // Mochitest sometimes causes resize after shutdown. See Bug 743190 if (!this.element) { return; } // Simplify when jsdom does getBoundingClientRect(). See Bug 717269 var dimensions = this.getDimensions(); if (dimensions) { this.onResize(dimensions); } }; /** * Make ourselves visually similar to the input element, and make the input * element transparent so our background shines through */ Inputter.prototype.getDimensions = function() { // Remove this when jsdom does getBoundingClientRect(). See Bug 717269 if (!this.element.getBoundingClientRect) { return undefined; } var fixedLoc = {}; var currentElement = this.element.parentNode; while (currentElement && currentElement.nodeName !== '#document') { var style = this.document.defaultView.getComputedStyle(currentElement, ''); if (style) { var position = style.getPropertyValue('position'); if (position === 'absolute' || position === 'fixed') { var bounds = currentElement.getBoundingClientRect(); fixedLoc.top = bounds.top; fixedLoc.left = bounds.left; break; } } currentElement = currentElement.parentNode; } var rect = this.element.getBoundingClientRect(); return { top: rect.top - (fixedLoc.top || 0) + 1, height: rect.bottom - rect.top - 1, left: rect.left - (fixedLoc.left || 0) + 2, width: rect.right - rect.left }; }; /** * Handler for the input-element.onMouseUp event */ Inputter.prototype.onMouseUp = function(ev) { this._checkAssignment(); }; /** * Handler for the Requisition.textChanged event */ Inputter.prototype.textChanged = function() { if (!this.document) { return; // This can happen post-destroy() } if (this._caretChange == null) { // We weren't expecting a change so this was requested by the hint system // we should move the cursor to the end of the 'changed section', and the // best we can do for that right now is the end of the current argument. this._caretChange = Caret.TO_ARG_END; } var newStr = this.requisition.toString(); var input = this.getInputState(); input.typed = newStr; this._processCaretChange(input); if (this.element.value !== newStr) { this.element.value = newStr; } this.onInputChange({ inputState: input }); }; /** * Various ways in which we need to manipulate the caret/selection position. * A value of null means we're not expecting a change */ var Caret = { /** * We are expecting changes, but we don't need to move the cursor */ NO_CHANGE: 0, /** * We want the entire input area to be selected */ SELECT_ALL: 1, /** * The whole input has changed - push the cursor to the end */ TO_END: 2, /** * A part of the input has changed - push the cursor to the end of the * changed section */ TO_ARG_END: 3 }; /** * If this._caretChange === Caret.TO_ARG_END, we alter the input object to move * the selection start to the end of the current argument. * @param input An object shaped like { typed:'', cursor: { start:0, end:0 }} */ Inputter.prototype._processCaretChange = function(input) { var start, end; switch (this._caretChange) { case Caret.SELECT_ALL: start = 0; end = input.typed.length; break; case Caret.TO_END: start = input.typed.length; end = input.typed.length; break; case Caret.TO_ARG_END: // There could be a fancy way to do this involving assignment/arg math // but it doesn't seem easy, so we cheat a move the cursor to just before // the next space, or the end of the input start = input.cursor.start; do { start++; } while (start < input.typed.length && input.typed[start - 1] !== ' '); end = start; break; default: start = input.cursor.start; end = input.cursor.end; break; } start = (start > input.typed.length) ? input.typed.length : start; end = (end > input.typed.length) ? input.typed.length : end; var newInput = { typed: input.typed, cursor: { start: start, end: end } }; if (this.element.selectionStart !== start) { this.element.selectionStart = start; } if (this.element.selectionEnd !== end) { this.element.selectionEnd = end; } this._checkAssignment(start); this._caretChange = null; return newInput; }; /** * To be called internally whenever we think that the current assignment might * have changed, typically on mouse-clicks or key presses. * @param start Optional - if specified, the cursor position to use in working * out the current assignment. This is needed because setting the element * selection start is only recognised when the event loop has finished */ Inputter.prototype._checkAssignment = function(start) { if (start == null) { start = this.element.selectionStart; } var newAssignment = this.requisition.getAssignmentAt(start); if (this.assignment !== newAssignment) { if (this.assignment.param.type.onLeave) { this.assignment.param.type.onLeave(this.assignment); } this.assignment = newAssignment; this.onAssignmentChange({ assignment: this.assignment }); if (this.assignment.param.type.onEnter) { this.assignment.param.type.onEnter(this.assignment); } } else { if (this.assignment && this.assignment.param.type.onChange) { this.assignment.param.type.onChange(this.assignment); } } // This is slightly nasty - the focusManager generally relies on people // telling it what it needs to know (which makes sense because the event // system to do it with events would be unnecessarily complex). However // requisition doesn't know about the focusManager either. So either one // needs to know about the other, or a third-party needs to break the // deadlock. These 2 lines are all we're quibbling about, so for now we hack if (this.focusManager) { this.focusManager.setError(this.assignment.message); } }; /** * Set the input field to a value, for external use. * This function updates the data model. It sets the caret to the end of the * input. It does not make any similarity checks so calling this function with * it's current value resets the cursor position. * It does not execute the input or affect the history. * This function should not be called internally, by Inputter and never as a * result of a keyboard event on this.element or bug 676520 could be triggered. */ Inputter.prototype.setInput = function(str) { this._caretChange = Caret.TO_END; return this.requisition.update(str); }; /** * Counterpart to |setInput| for moving the cursor. * @param cursor An object shaped like { start: x, end: y } */ Inputter.prototype.setCursor = function(cursor) { this._caretChange = Caret.NO_CHANGE; this._processCaretChange({ typed: this.element.value, cursor: cursor }); }; /** * Focus the input element */ Inputter.prototype.focus = function() { this.element.focus(); this._checkAssignment(); }; /** * Ensure certain keys (arrows, tab, etc) that we would like to handle * are not handled by the browser */ Inputter.prototype.onKeyDown = function(ev) { if (ev.keyCode === KeyEvent.DOM_VK_UP || ev.keyCode === KeyEvent.DOM_VK_DOWN) { ev.preventDefault(); return; } // The following keys do not affect the state of the command line so we avoid // informing the focusManager about keyboard events that involve these keys if (ev.keyCode === KeyEvent.DOM_VK_F1 || ev.keyCode === KeyEvent.DOM_VK_ESCAPE || ev.keyCode === KeyEvent.DOM_VK_UP || ev.keyCode === KeyEvent.DOM_VK_DOWN) { return; } if (this.focusManager) { this.focusManager.onInputChange(); } if (ev.keyCode === KeyEvent.DOM_VK_TAB) { this.lastTabDownAt = 0; if (!ev.shiftKey) { ev.preventDefault(); // Record the timestamp of this TAB down so onKeyUp can distinguish // focus from TAB in the CLI. this.lastTabDownAt = ev.timeStamp; } if (ev.metaKey || ev.altKey || ev.crtlKey) { if (this.document.commandDispatcher) { this.document.commandDispatcher.advanceFocus(); } else { this.element.blur(); } } } }; /** * Handler for use with DOM events, which just calls the promise enabled * handleKeyUp function but checks the exit state of the promise so we know * if something went wrong. */ Inputter.prototype.onKeyUp = function(ev) { this.handleKeyUp(ev).then(null, util.errorHandler); }; /** * The main keyboard processing loop * @return A promise that resolves (to undefined) when the actions kicked off * by this handler are completed. */ Inputter.prototype.handleKeyUp = function(ev) { if (this.focusManager && ev.keyCode === KeyEvent.DOM_VK_F1) { this.focusManager.helpRequest(); return RESOLVED; } if (this.focusManager && ev.keyCode === KeyEvent.DOM_VK_ESCAPE) { this.focusManager.removeHelp(); return RESOLVED; } if (ev.keyCode === KeyEvent.DOM_VK_UP) { return this._handleUpArrow(); } if (ev.keyCode === KeyEvent.DOM_VK_DOWN) { return this._handleDownArrow(); } if (ev.keyCode === KeyEvent.DOM_VK_RETURN) { return this._handleReturn(); } if (ev.keyCode === KeyEvent.DOM_VK_TAB && !ev.shiftKey) { return this._handleTab(ev); } // Give the scratchpad (if enabled) a chance to activate if (this.scratchpad && this.scratchpad.shouldActivate(ev)) { if (this.scratchpad.activate(this.element.value)) { return this.requisition.update(''); } return RESOLVED; } if (this._previousValue === this.element.value) { return RESOLVED; } this._scrollingThroughHistory = false; this._caretChange = Caret.NO_CHANGE; this._completed = this.requisition.update(this.element.value); this._previousValue = this.element.value; return this._completed.then(function(updated) { // Abort UI changes if this UI update has been overtaken if (updated) { this._choice = null; this.onChoiceChange({ choice: this._choice }); } }.bind(this)); }; /** * See also _handleDownArrow for some symmetry */ Inputter.prototype._handleUpArrow = function() { if (this.tooltip && this.tooltip.isMenuShowing) { this.changeChoice(-1); return RESOLVED; } if (this.element.value === '' || this._scrollingThroughHistory) { this._scrollingThroughHistory = true; return this.requisition.update(this.history.backward()); } // If the user is on a valid value, then we increment the value, but if // they've typed something that's not right we page through predictions if (this.assignment.getStatus() === Status.VALID) { this.requisition.increment(this.assignment); // See notes on focusManager.onInputChange in onKeyDown if (this.focusManager) { this.focusManager.onInputChange(); } } else { this.changeChoice(-1); } return RESOLVED; }; /** * See also _handleUpArrow for some symmetry */ Inputter.prototype._handleDownArrow = function() { if (this.tooltip && this.tooltip.isMenuShowing) { this.changeChoice(+1); return RESOLVED; } if (this.element.value === '' || this._scrollingThroughHistory) { this._scrollingThroughHistory = true; return this.requisition.update(this.history.forward()); } // See notes above for the UP key if (this.assignment.getStatus() === Status.VALID) { this.requisition.decrement(this.assignment, this.requisition.executionContext); // See notes on focusManager.onInputChange in onKeyDown if (this.focusManager) { this.focusManager.onInputChange(); } } else { this.changeChoice(+1); } return RESOLVED; }; /** * RETURN checks status and might exec */ Inputter.prototype._handleReturn = function() { // Deny RETURN unless the command might work if (this.requisition.status === Status.VALID) { this._scrollingThroughHistory = false; this.history.add(this.element.value); this.requisition.exec(); } else { // If we can't execute the command, but there is a menu choice to use // then use it. if (!this.tooltip.selectChoice()) { this.focusManager.setError(true); } } this._choice = null; return RESOLVED; }; /** * Warning: We get TAB events for more than just the user pressing TAB in our * input element. */ Inputter.prototype._handleTab = function(ev) { // Being able to complete 'nothing' is OK if there is some context, but // when there is nothing on the command line it just looks bizarre. var hasContents = (this.element.value.length > 0); // If the TAB keypress took the cursor from another field to this one, // then they get the keydown/keypress, and we get the keyup. In this // case we don't want to do any completion. // If the time of the keydown/keypress of TAB was close (i.e. within // 1 second) to the time of the keyup then we assume that we got them // both, and do the completion. if (hasContents && this.lastTabDownAt + 1000 > ev.timeStamp) { // It's possible for TAB to not change the input, in which case the // textChanged event will not fire, and the caret move will not be // processed. So we check that this is done first this._caretChange = Caret.TO_ARG_END; var inputState = this.getInputState(); this._processCaretChange(inputState); if (this._choice == null) { this._choice = 0; } // The changes made by complete may happen asynchronously, so after the // the call to complete() we should avoid making changes before the end // of the event loop this._completed = this.requisition.complete(inputState.cursor, this._choice); this._previousValue = this.element.value; } this.lastTabDownAt = 0; this._scrollingThroughHistory = false; return this._completed.then(function(updated) { // Abort UI changes if this UI update has been overtaken if (updated) { this._choice = null; this.onChoiceChange({ choice: this._choice }); } }.bind(this)); }; /** * Used by onKeyUp for UP/DOWN to change the current choice from an options * menu. */ Inputter.prototype.changeChoice = function(amount) { if (this._choice == null) { this._choice = 0; } // There's an annoying up is down thing here, the menu is presented // with the zeroth index at the top working down, so the UP arrow needs // pick the choice below because we're working down this._choice += amount; this.onChoiceChange({ choice: this._choice }); }; /** * Accessor for the assignment at the cursor. * i.e Requisition.getAssignmentAt(cursorPos); */ Inputter.prototype.getCurrentAssignment = function() { var start = this.element.selectionStart; return this.requisition.getAssignmentAt(start); }; /** * Pull together an input object, which may include XUL hacks */ Inputter.prototype.getInputState = function() { var input = { typed: this.element.value, cursor: { start: this.element.selectionStart, end: this.element.selectionEnd } }; // Workaround for potential XUL bug 676520 where textbox gives incorrect // values for its content if (input.typed == null) { input = { typed: '', cursor: { start: 0, end: 0 } }; } // Workaround for a Bug 717268 (which is really a jsdom bug) if (input.cursor.start == null) { input.cursor.start = 0; } return input; }; exports.Inputter = Inputter; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/history', ['require', 'exports', 'module' ], function(require, exports, module) { 'use strict'; /** * A History object remembers commands that have been entered in the past and * provides an API for accessing them again. * See Bug 681340: Search through history (like C-r in bash)? */ function History() { // This is the actual buffer where previous commands are kept. // 'this._buffer[0]' should always be equal the empty string. This is so // that when you try to go in to the "future", you will just get an empty // command. this._buffer = ['']; // This is an index in to the history buffer which points to where we // currently are in the history. this._current = 0; } /** * Avoid memory leaks */ History.prototype.destroy = function() { delete this._buffer; }; /** * Record and save a new command in the history. */ History.prototype.add = function(command) { this._buffer.splice(1, 0, command); this._current = 0; }; /** * Get the next (newer) command from history. */ History.prototype.forward = function() { if (this._current > 0 ) { this._current--; } return this._buffer[this._current]; }; /** * Get the previous (older) item from history. */ History.prototype.backward = function() { if (this._current < this._buffer.length - 1) { this._current++; } return this._buffer[this._current]; }; exports.History = History; }); define("text!gcli/ui/inputter.css", [], ""); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/ui/completer', ['require', 'exports', 'module' , 'util/promise', 'util/util', 'util/domtemplate', 'text!gcli/ui/completer.html'], function(require, exports, module) { 'use strict'; var promise = require('util/promise'); var util = require('util/util'); var domtemplate = require('util/domtemplate'); var completerHtml = require('text!gcli/ui/completer.html'); /** * Completer is an 'input-like' element that sits an input element annotating * it with visual goodness. * @param options Object containing user customization properties, including: * - scratchpad (default=none) A way to move JS content to custom JS editor * @param components Object that links to other UI components. GCLI provided: * - requisition: A GCLI Requisition object whose state is monitored * - element: Element to use as root * - autoResize: (default=false): Should we attempt to sync the dimensions of * the complete element with the input element. */ function Completer(options, components) { this.requisition = components.requisition; this.scratchpad = options.scratchpad; this.input = { typed: '', cursor: { start: 0, end: 0 } }; this.choice = 0; this.element = components.element; this.element.classList.add('gcli-in-complete'); this.element.setAttribute('tabindex', '-1'); this.element.setAttribute('aria-live', 'polite'); this.document = this.element.ownerDocument; this.inputter = components.inputter; this.inputter.onInputChange.add(this.update, this); this.inputter.onAssignmentChange.add(this.update, this); this.inputter.onChoiceChange.add(this.update, this); this.autoResize = components.autoResize; if (this.autoResize) { this.inputter.onResize.add(this.resized, this); var dimensions = this.inputter.getDimensions(); if (dimensions) { this.resized(dimensions); } } this.template = util.toDom(this.document, completerHtml); // We want the spans to line up without the spaces in the template util.removeWhitespace(this.template, true); this.update(); } /** * Avoid memory leaks */ Completer.prototype.destroy = function() { this.inputter.onInputChange.remove(this.update, this); this.inputter.onAssignmentChange.remove(this.update, this); this.inputter.onChoiceChange.remove(this.update, this); if (this.autoResize) { this.inputter.onResize.remove(this.resized, this); } delete this.document; delete this.element; delete this.template; delete this.inputter; }; /** * Ensure that the completion element is the same size and the inputter element */ Completer.prototype.resized = function(ev) { this.element.style.top = ev.top + 'px'; this.element.style.height = ev.height + 'px'; this.element.style.lineHeight = ev.height + 'px'; this.element.style.left = ev.left + 'px'; this.element.style.width = ev.width + 'px'; }; /** * Bring the completion element up to date with what the requisition says */ Completer.prototype.update = function(ev) { this.choice = (ev && ev.choice != null) ? ev.choice : 0; var data = this._getCompleterTemplateData(); var template = this.template.cloneNode(true); domtemplate.template(template, data, { stack: 'completer.html' }); util.clearElement(this.element); while (template.hasChildNodes()) { this.element.appendChild(template.firstChild); } }; /** * Calculate the properties required by the template process for completer.html */ Completer.prototype._getCompleterTemplateData = function() { // Some of the data created by this function can be calculated synchronously // but other parts depend on predictions which are asynchronous. var promisedDirectTabText = promise.defer(); var promisedArrowTabText = promise.defer(); var promisedEmptyParameters = promise.defer(); var input = this.inputter.getInputState(); var current = this.requisition.getAssignmentAt(input.cursor.start); var predictionPromise; if (input.typed.trim().length !== 0) { predictionPromise = current.getPredictionAt(this.choice); } // If anything goes wrong, we pass the error on to all the child promises var onError = function(ex) { promisedDirectTabText.reject(ex); promisedArrowTabText.reject(ex); promisedEmptyParameters.reject(ex); }; promise.resolve(predictionPromise).then(function(prediction) { // directTabText is for when the current input is a prefix of the completion // arrowTabText is for when we need to use an -> to show what will be used var directTabText = ''; var arrowTabText = ''; var emptyParameters = []; if (input.typed.trim().length !== 0) { var cArg = current.arg; if (prediction) { var tabText = prediction.name; var existing = cArg.text; // Normally the cursor being just before whitespace means that you are // 'in' the previous argument, which means that the prediction is based // on that argument, however NamedArguments break this by having 2 parts // so we need to prepend the tabText with a space for NamedArguments, // but only when there isn't already a space at the end of the prefix // (i.e. ' --name' not ' --name ') if (current.isInName()) { tabText = ' ' + tabText; } if (existing !== tabText) { // Decide to use directTabText or arrowTabText // Strip any leading whitespace from the user inputted value because // the tabText will never have leading whitespace. var inputValue = existing.replace(/^\s*/, ''); var isStrictCompletion = tabText.indexOf(inputValue) === 0; if (isStrictCompletion && input.cursor.start === input.typed.length) { // Display the suffix of the prediction as the completion var numLeadingSpaces = existing.match(/^(\s*)/)[0].length; directTabText = tabText.slice(existing.length - numLeadingSpaces); } else { // Display the '-> prediction' at the end of the completer element // \u21E5 is the JS escape right arrow arrowTabText = '\u21E5 ' + tabText; } } } else { // There's no prediction, but if this is a named argument that needs a // value (that is without any) then we need to show that one is needed // For example 'git commit --message ', clearly needs some more text if (cArg.type === 'NamedArgument' && cArg.valueArg == null) { emptyParameters.push('<' + current.param.type.name + '>\u00a0'); } } } // Add a space between the typed text (+ directTabText) and the hints, // making sure we don't add 2 sets of padding if (directTabText !== '') { directTabText += '\u00a0'; } else if (!this.requisition.typedEndsWithSeparator()) { emptyParameters.unshift('\u00a0'); } // Calculate the list of parameters to be filled in // We generate an array of emptyParameter markers for each positional // parameter to the current command. // Generally each emptyParameter marker begins with a space to separate it // from whatever came before, unless what comes before ends in a space. this.requisition.getAssignments().forEach(function(assignment) { // Named arguments are handled with a group [options] marker if (!assignment.param.isPositionalAllowed) { return; } // No hints if we've got content for this parameter if (assignment.arg.toString().trim() !== '') { return; } if (directTabText !== '' && current === assignment) { return; } var text = (assignment.param.isDataRequired) ? '<' + assignment.param.name + '>\u00a0' : '[' + assignment.param.name + ']\u00a0'; emptyParameters.push(text); }.bind(this)); var command = this.requisition.commandAssignment.value; var addOptionsMarker = false; // We add an '[options]' marker when there are named parameters that are // not filled in and not hidden, and we don't have any directTabText if (command && command.hasNamedParameters) { command.params.forEach(function(param) { var arg = this.requisition.getAssignment(param.name).arg; if (!param.isPositionalAllowed && !param.hidden && arg.type === 'BlankArgument') { addOptionsMarker = true; } }, this); } if (addOptionsMarker) { // Add an nbsp if we don't have one at the end of the input or if // this isn't the first param we've mentioned emptyParameters.push('[options]\u00a0'); } promisedDirectTabText.resolve(directTabText); promisedArrowTabText.resolve(arrowTabText); promisedEmptyParameters.resolve(emptyParameters); }.bind(this), onError); return { statusMarkup: this._getStatusMarkup(input), unclosedJs: this._getUnclosedJs(), scratchLink: this._getScratchLink(), directTabText: promisedDirectTabText.promise, arrowTabText: promisedArrowTabText.promise, emptyParameters: promisedEmptyParameters.promise }; }; /** * Calculate the statusMarkup required to show wavy lines underneath the input * text (like that of an inline spell-checker) which used by the template * process for completer.html */ Completer.prototype._getStatusMarkup = function(input) { // statusMarkup is wrapper around requisition.getInputStatusMarkup converting // space to   in the string member (for HTML display) and status to an // appropriate class name (i.e. lower cased, prefixed with gcli-in-) var statusMarkup = this.requisition.getInputStatusMarkup(input.cursor.start); statusMarkup.forEach(function(member) { member.string = member.string.replace(/ /g, '\u00a0'); // i.e.   member.className = 'gcli-in-' + member.status.toString().toLowerCase(); }, this); return statusMarkup; }; /** * Is the entered command a JS command with no closing '}'? */ Completer.prototype._getUnclosedJs = function() { // TWEAK: This code should be considered for promotion to Requisition var command = this.requisition.commandAssignment.value; return command && command.name === '{' && this.requisition.getAssignment(0).arg.suffix.indexOf('}') === -1; }; /** * The text for the 'jump to scratchpad' feature, or '' if it is disabled */ Completer.prototype._getScratchLink = function() { var command = this.requisition.commandAssignment.value; return this.scratchpad && command && command.name === '{' ? this.scratchpad.linkText : ''; }; exports.Completer = Completer; }); define("text!gcli/ui/completer.html", [], "\n" + "\n" + " \n" + " \n" + " \n" + "