diff --git a/gcli.js b/gcli.js index 5708a0d9..eb544be8 100644 --- a/gcli.js +++ b/gcli.js @@ -19,53 +19,16 @@ exports.gcliHome = __dirname; -var system = require('./lib/gcli/api').createSystem(); +var system = require('./lib/gcli/system').createSystem(); -/* - * GCLI is built from a number of components (called items) composed as - * required for each environment. - * When adding to or removing from this list, we should keep the basics in sync - * with the other environments. - * See: - * - lib/gcli/index.js: Generic basic set (without commands) - * - lib/gcli/demo.js: Adds demo commands to basic set for use in web demo - * - gcli.js: Add commands to basic set for use in Node command line - * - mozilla/gcli/index.js: From scratch listing for Firefox - * - lib/gcli/connectors/index.js: Client only items when executing remotely - * - lib/gcli/connectors/direct.js: Test items for connecting to in-process GCLI - */ var items = [ - require('./lib/gcli/index').items, - - require('./lib/gcli/cli').items, - require('./lib/gcli/commands/clear').items, - // require('./lib/gcli/commands/connect').items, - require('./lib/gcli/commands/context').items, - require('./lib/gcli/commands/exec').items, - require('./lib/gcli/commands/global').items, - require('./lib/gcli/commands/help').items, - require('./lib/gcli/commands/intro').items, - require('./lib/gcli/commands/lang').items, - require('./lib/gcli/commands/mocks').items, - require('./lib/gcli/commands/pref').items, - require('./lib/gcli/commands/preflist').items, - require('./lib/gcli/commands/test').items, - - require('./lib/gcli/commands/demo/alert').items, - // require('./lib/gcli/commands/demo/bugs').items, - // require('./lib/gcli/commands/demo/demo').items, - require('./lib/gcli/commands/demo/echo').items, - // require('./lib/gcli/commands/demo/edit').items, - // require('./lib/gcli/commands/demo/git').items, - // require('./lib/gcli/commands/demo/hg').items, - require('./lib/gcli/commands/demo/sleep').items, - // require('./lib/gcli/commands/demo/theme').items, + require('./lib/gcli/items/basic').items, + require('./lib/gcli/items/ui').items, + require('./lib/gcli/items/remote').items, + require('./lib/gcli/items/standard').items, + require('./lib/gcli/items/demo').items, + require('./lib/gcli/items/server').items, - require('./lib/gcli/commands/server/exit').items, - require('./lib/gcli/commands/server/firefox').items, - require('./lib/gcli/commands/server/orion').items, - require('./lib/gcli/commands/server/server').items, - require('./lib/gcli/commands/server/standard').items ].reduce(function(prev, curr) { return prev.concat(curr); }, []); system.addItems(items); @@ -116,7 +79,7 @@ function logResults(output) { requisition.updateExec(command) .then(logResults) .then(extraActions) - .then(null, util.errorHandler); + .catch(util.errorHandler); /** * Start a NodeJS REPL to execute commands @@ -133,10 +96,8 @@ function startRepl() { if (command.length !== 0) { requisition.updateExec(command) .then(logResults) - .then( - function() { callback(); }, - function(ex) { util.errorHandler(ex); callback(); } - ); + .then(function() { callback(); }) + .catch(function(ex) { util.errorHandler(ex); callback(); }); } }; diff --git a/index.html b/index.html index 6f1cc562..918264f9 100644 --- a/index.html +++ b/index.html @@ -25,19 +25,18 @@ paths: { i18n: '../scripts/i18n', text: '../scripts/text' } }); - var modules = [ 'gcli/index', 'gcli/demo', 'gcli/test/index' ]; + var modules = [ 'gcli/index', 'gcli/items/demo', 'gcli/test/index' ]; require(modules, function(gcli, demo, test) { - // Add demo commands. You'll probably want to replace this with - // your own set of commands - gcli.addItems(demo.items); + // Add the commands/types/converters as required + var system = gcli.createSystem(); + system.addItems(gcli.items); // Common infrastructure: types, etc + system.addItems(gcli.commandItems); // Common set of useful commands + system.addItems(demo.items); // Extra demo commands - // To run commands remotely, - // Hook GCLI into this page, using gcli-root above - var options = {}; - gcli.createTerminal(options).then(function() { - // Run the unit test at each startup. - test.run(options); - }).then(null, console.error); + gcli.createTerminal(system).then(function(terminal) { + terminal.language.showIntro(); // Intro text + test.run(terminal, false); // Run the unit test at each startup + }).catch(console.error.bind(console)); }); diff --git a/lib/gcli/api.js b/lib/gcli/api.js deleted file mode 100644 index c2d0f71e..00000000 --- a/lib/gcli/api.js +++ /dev/null @@ -1,217 +0,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. - */ - -'use strict'; - -var Promise = require('./util/promise').Promise; -var Commands = require('./commands/commands').Commands; -var Connectors = require('./connectors/connectors').Connectors; -var Converters = require('./converters/converters').Converters; -var Fields = require('./fields/fields').Fields; -var Languages = require('./languages/languages').Languages; -var Settings = require('./settings').Settings; -var Types = require('./types/types').Types; - -/** - * This is the heart of the API that we expose to the outside - */ -exports.createSystem = function() { - - var components = { - connector: new Connectors(), - converter: new Converters(), - field: new Fields(), - language: new Languages(), - type: new Types() - }; - components.setting = new Settings(components.type); - components.command = new Commands(components.type); - - var getItemType = function(item) { - if (item.item) { - return item.item; - } - // Some items are registered using the constructor so we need to check - // the prototype for the the type of the item - return (item.prototype && item.prototype.item) ? - item.prototype.item : 'command'; - }; - - var addItem = function(item) { - components[getItemType(item)].add(item); - }; - - var removeItem = function(item) { - components[getItemType(item)].remove(item); - }; - - /** - * loadableModules is a lookup of names to module loader functions (like - * the venerable 'require') to which we can pass a name and get back a - * JS object (or a promise of a JS object). This allows us to have custom - * loaders to get stuff from the filesystem etc. - */ - var loadableModules = {}; - - /** - * loadedModules is a lookup by name of the things returned by the functions - * in loadableModules so we can track what we need to unload / reload. - */ - var loadedModules = {}; - - var unloadModule = function(name) { - var existingModule = loadedModules[name]; - if (existingModule != null) { - existingModule.items.forEach(removeItem); - } - delete loadedModules[name]; - }; - - var loadModule = function(name) { - var existingModule = loadedModules[name]; - unloadModule(name); - - // And load the new items - try { - var loader = loadableModules[name]; - return Promise.resolve(loader(name)).then(function(newModule) { - if (existingModule === newModule) { - return; - } - - if (newModule == null) { - throw 'Module \'' + name + '\' not found'; - } - - if (newModule.items == null || typeof newModule.items.forEach !== 'function') { - console.log('Exported properties: ' + Object.keys(newModule).join(', ')); - throw 'Module \'' + name + '\' has no \'items\' array export'; - } - - newModule.items.forEach(addItem); - - loadedModules[name] = newModule; - }); - } - catch (ex) { - console.error(ex); - return Promise.reject('Failure when loading \'' + name + '\''); - } - }; - - var pendingChanges = false; - - var api = { - addItems: function(items) { - items.forEach(addItem); - }, - - removeItems: function(items) { - items.forEach(removeItem); - }, - - addItemsByModule: function(names, options) { - options = options || {}; - if (typeof names === 'string') { - names = [ names ]; - } - names.forEach(function(name) { - if (options.loader == null) { - options.loader = function(name) { - return require(name); - }; - } - loadableModules[name] = options.loader; - - if (options.delayedLoad) { - pendingChanges = true; - } - else { - loadModule(name).then(null, console.error); - } - }); - }, - - removeItemsByModule: function(name) { - delete loadableModules[name]; - unloadModule(name); - }, - - load: function() { - if (!pendingChanges) { - return Promise.resolve(); - } - - // clone loadedModules, so we can remove what is left at the end - var modules = Object.keys(loadedModules).map(function(name) { - return loadedModules[name]; - }); - - var promises = Object.keys(loadableModules).map(function(name) { - delete modules[name]; - return loadModule(name); - }); - - Object.keys(modules).forEach(unloadModule); - pendingChanges = false; - - return Promise.all(promises); - } - }; - - Object.defineProperty(api, 'commands', { - get: function() { return components.command; }, - set: function(commands) { components.command = commands; }, - enumerable: true - }); - - Object.defineProperty(api, 'connectors', { - get: function() { return components.connector; }, - enumerable: true - }); - - Object.defineProperty(api, 'converters', { - get: function() { return components.converter; }, - enumerable: true - }); - - Object.defineProperty(api, 'fields', { - get: function() { return components.field; }, - enumerable: true - }); - - Object.defineProperty(api, 'languages', { - get: function() { return components.language; }, - enumerable: true - }); - - Object.defineProperty(api, 'settings', { - get: function() { return components.setting; }, - enumerable: true - }); - - Object.defineProperty(api, 'types', { - get: function() { return components.type; }, - set: function(types) { - components.type = types; - components.command.types = types; - components.setting.types = types; - }, - enumerable: true - }); - - return api; -}; diff --git a/lib/gcli/cli.js b/lib/gcli/cli.js index 687cd7d5..0fdaee9f 100644 --- a/lib/gcli/cli.js +++ b/lib/gcli/cli.js @@ -100,16 +100,6 @@ var removeMapping = function(requisition) { instances.splice(index, 1); }; -/** - * Some manual intervention is needed in parsing the { command. - */ -function getEvalCommand(commands) { - if (getEvalCommand._cmd == null) { - getEvalCommand._cmd = commands.get(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 @@ -318,7 +308,7 @@ var evalCmd = { var reply = customEval(args.javascript); return context.typedData(typeof reply, reply); }, - isCommandRegexp: /^\s*{\s*/ + isCommandRegexp: /^\s*\{\s*/ }; exports.items = [ evalCmd ]; @@ -704,7 +694,8 @@ Object.defineProperty(Requisition.prototype, 'status', { */ Requisition.prototype.getStatusMessage = function() { if (this.commandAssignment.getStatus() !== Status.VALID) { - return l10n.lookup('cliUnknownCommand'); + return l10n.lookupFormat('cliUnknownCommand2', + [ this.commandAssignment.arg.text ]); } var assignments = this.getAssignments(); @@ -1786,8 +1777,8 @@ Requisition.prototype._split = function(args) { 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(this.system.commands), - new ScriptArgument()); + var command = this.system.commands.get(evalCmd.name); + conversion = new Conversion(command, new ScriptArgument()); this._setAssignmentInternal(this.commandAssignment, conversion); return; } @@ -2045,7 +2036,7 @@ Requisition.prototype.exec = function(options) { typed = typed.replace(/\s*}\s*$/, ''); } - var output = new Output(this.conversionContext, { + var output = new Output({ command: command, args: args, typed: typed, @@ -2134,14 +2125,13 @@ exports.Requisition = Requisition; /** * A simple object to hold information about the output of a command */ -function Output(context, options) { +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.converters = context.system.converters; this.type = undefined; this.data = undefined; @@ -2186,7 +2176,8 @@ Output.prototype.complete = function(data, error) { * Call converters.convert using the data in this Output object */ Output.prototype.convert = function(type, conversionContext) { - return this.converters.convert(this.data, this.type, type, conversionContext); + var converters = conversionContext.system.converters; + return converters.convert(this.data, this.type, type, conversionContext); }; Output.prototype.toJson = function() { diff --git a/lib/gcli/commands/commands.js b/lib/gcli/commands/commands.js index be2cf775..e76ea8b7 100644 --- a/lib/gcli/commands/commands.js +++ b/lib/gcli/commands/commands.js @@ -161,8 +161,11 @@ function Command(types, commandSpec) { /** * JSON serializer that avoids non-serializable data + * @param customProps Array of strings containing additional properties which, + * if specified in the command spec, will be included in the JSON. Normally we + * transfer only the properties required for GCLI to function. */ -Command.prototype.toJson = function() { +Command.prototype.toJson = function(customProps) { var json = { item: 'command', name: this.name, @@ -170,6 +173,7 @@ Command.prototype.toJson = function() { returnType: this.returnType, isParent: (this.exec == null) }; + if (this.description !== l10n.lookup('canonDescNone')) { json.description = this.description; } @@ -179,6 +183,15 @@ Command.prototype.toJson = function() { if (this.hidden != null) { json.hidden = this.hidden; } + + if (Array.isArray(customProps)) { + customProps.forEach(function(prop) { + if (this[prop] != null) { + json[prop] = this[prop]; + } + }.bind(this)); + } + return json; }; @@ -327,9 +340,14 @@ exports.Parameter = Parameter; /** * A store for a list of commands + * @param types Each command uses a set of Types to parse its parameters so the + * Commands container needs access to the list of available types. + * @param location String that, if set will force all commands to have a + * matching runAt property to be accepted */ -function Commands(types) { +function Commands(types, location) { this.types = types; + this.location = location; // A lookup hash of our registered commands this._commands = {}; @@ -344,12 +362,16 @@ function Commands(types) { /** * Add a command to the list 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 + * @return The new command, or null if a location property has been set and the + * commandSpec doesn't have a matching runAt property. */ Commands.prototype.add = function(commandSpec) { + if (this.location != null && commandSpec.runAt != null && + commandSpec.runAt !== this.location) { + return; + } + if (this._commands[commandSpec.name] != null) { // Roughly commands.remove() without the event call, which we do later delete this._commands[commandSpec.name]; @@ -416,14 +438,17 @@ Commands.prototype.getAll = function() { /** * Get access to the stored commandMetaDatas (i.e. before they were made into * instances of Command/Parameters) so we can remote them. + * @param customProps Array of strings containing additional properties which, + * if specified in the command spec, will be included in the JSON. Normally we + * transfer only the properties required for GCLI to function. */ -Commands.prototype.getCommandSpecs = function() { +Commands.prototype.getCommandSpecs = function(customProps) { var commandSpecs = []; Object.keys(this._commands).forEach(function(name) { var command = this._commands[name]; if (!command.noRemote) { - commandSpecs.push(command.toJson()); + commandSpecs.push(command.toJson(customProps)); } }.bind(this)); diff --git a/lib/gcli/commands/connect.js b/lib/gcli/commands/connect.js index bc7d7184..beb24083 100644 --- a/lib/gcli/commands/connect.js +++ b/lib/gcli/commands/connect.js @@ -18,11 +18,12 @@ var l10n = require('../util/l10n'); var cli = require('../cli'); +var GcliFront = require('../connectors/remoted').GcliFront; /** * A lookup of the current connection */ -var connections = {}; +var fronts = {}; /** * 'connection' type @@ -32,8 +33,8 @@ var connection = { name: 'connection', parent: 'selection', lookup: function() { - return Object.keys(connections).map(function(prefix) { - return { name: prefix, value: connections[prefix] }; + return Object.keys(fronts).map(function(prefix) { + return { name: prefix, value: fronts[prefix] }; }); } }; @@ -87,19 +88,19 @@ var connect = { returnType: 'string', exec: function(args, context) { - if (connections[args.prefix] != null) { + if (fronts[args.prefix] != null) { throw new Error(l10n.lookupFormat('connectDupReply', [ args.prefix ])); } - var connector = args.method || context.system.connectors.get('xhr'); + args.method = args.method || context.system.connectors.get('xhr'); - return connector.connect(args.url).then(function(connection) { - // Nasty: stash the prefix on the connection to help us tidy up - connection.prefix = args.prefix; - connections[args.prefix] = connection; + return GcliFront.create(args.method, args.url).then(function(front) { + // Nasty: stash the prefix on the front to help us tidy up + front.prefix = args.prefix; + fronts[args.prefix] = front; - return connection.call('specs').then(function(specs) { - var remoter = this.createRemoter(args.prefix, connection); + return front.specs().then(function(specs) { + var remoter = this.createRemoter(args.prefix, front); var commands = cli.getMapping(context).requisition.system.commands; commands.addProxyCommands(specs, remoter, args.prefix, args.url); @@ -116,7 +117,7 @@ var connect = { * When we register a set of remote commands, we need to provide a proxy * executor. This is that executor. */ - createRemoter: function(prefix, connection) { + createRemoter: function(prefix, front) { return function(cmdArgs, context) { var typed = context.typed; @@ -126,12 +127,7 @@ var connect = { typed = typed.substring(prefix.length).replace(/^ */, ''); } - var data = { - typed: typed, - args: cmdArgs - }; - - return connection.call('execute', data).then(function(reply) { + return front.execute(typed).then(function(reply) { var typedData = context.typedData(reply.type, reply.data); if (!reply.error) { return typedData; @@ -162,11 +158,11 @@ var disconnect = { returnType: 'string', exec: function(args, context) { - var connection = args.prefix; - return connection.disconnect().then(function() { + var front = args.prefix; + return front.connection.disconnect().then(function() { var commands = cli.getMapping(context).requisition.system.commands; - var removed = commands.removeProxyCommands(connection.prefix); - delete connections[connection.prefix]; + var removed = commands.removeProxyCommands(front.prefix); + delete fronts[front.prefix]; return l10n.lookupFormat('disconnectReply', [ removed.length ]); }); } diff --git a/lib/gcli/commands/demo/bugs.js b/lib/gcli/commands/demo/bugs.js deleted file mode 100644 index ef1ce409..00000000 --- a/lib/gcli/commands/demo/bugs.js +++ /dev/null @@ -1,134 +0,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. - */ - -'use strict'; - -var Promise = require('../../util/promise').Promise; - -exports.items = [ - { - item: 'converter', - from: 'bugz', - to: 'view', - exec: function(bugz, context) { - return { - html: - '
\n' + - '

\n' + - ' Open GCLI meta-bugs\n' + - ' (i.e. this search):\n' + - '

\n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - '
IDMilestonePriSummary
${bug.id}${bug.target_milestone}${bug.priority}${bug.summary}
\n' + - '
', - data: bugz - }; - } - }, - { - item: 'command', - name: 'bugz', - returnType: 'bugz', - description: 'List the GCLI bugs open in Bugzilla', - exec: function(args, context) { - return queryBugzilla(args, context).then(filterReply); - } - } -]; - -/** - * Simple wrapper for querying bugzilla. - * @see https://wiki.mozilla.org/Bugzilla:REST_API - * @see https://wiki.mozilla.org/Bugzilla:REST_API:Search - * @see http://www.bugzilla.org/docs/developer.html - * @see https://harthur.wordpress.com/2011/03/31/bz-js/ - * @see https://github.com/harthur/bz.js - */ -function queryBugzilla(args, context) { - return new Promise(function(resolve, reject) { - var url = 'https://api-dev.bugzilla.mozilla.org/1.1/bug?' + - 'short_desc=GCLI' + - '&short_desc_type=allwords' + - '&bug_status=UNCONFIRMED' + - '&bug_status=NEW' + - '&bug_status=ASSIGNED' + - '&bug_status=REOPENED'; - - var req = new XMLHttpRequest(); - req.open('GET', url, true); - req.setRequestHeader('Accept', 'application/json'); - req.setRequestHeader('Content-type', 'application/json'); - req.onreadystatechange = function(event) { - if (req.readyState == 4) { - if (req.status >= 300 || req.status < 200) { - reject('Error: ' + JSON.stringify(req)); - return; - } - - try { - var json = JSON.parse(req.responseText); - if (json.error) { - reject('Error: ' + json.error.message); - } - else { - resolve(json); - } - } - catch (ex) { - reject('Invalid response: ' + ex + ': ' + req.responseText); - } - } - }; - req.send(); - }); -} - -/** - * Filter the output from Bugzilla for display - */ -function filterReply(json) { - json.bugs.forEach(function(bug) { - if (bug.target_milestone === '---') { - bug.target_milestone = 'Future'; - } - }); - - json.bugs.sort(function(bug1, bug2) { - var ms = bug1.target_milestone.localeCompare(bug2.target_milestone); - if (ms !== 0) { - return ms; - } - return bug1.priority.localeCompare(bug2.priority); - }); - - return json; -} diff --git a/lib/gcli/commands/demo/git.js b/lib/gcli/commands/demo/git.js deleted file mode 100644 index 63266dc3..00000000 --- a/lib/gcli/commands/demo/git.js +++ /dev/null @@ -1,276 +0,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. - */ - -'use strict'; - -exports.items = [ - { - // commitObject really needs some smarts, but for now it is a clone of string - item: 'type', - name: 'commitObject', - parent: 'string' - }, - { - // existingFile really needs some smarts, but for now it is a clone of string - item: 'type', - name: 'existingFile', - parent: 'string' - }, - { - // Parent 'git' command - item: 'command', - name: 'git', - description: 'Distributed revision control in a browser', - manual: 'Git is a fast, scalable, distributed revision control system' + - ' with an unusually rich command set that provides both' + - ' high-level operations and full access to internals.' - }, - { - // 'git add' command - item: 'command', - name: 'git add', - description: 'Add file contents to the index', - manual: 'This command updates the index using the current content found in the working tree, to prepare the content staged for the next commit. It typically adds the current content of existing paths as a whole, but with some options it can also be used to add content with only part of the changes made to the working tree files applied, or remove paths that do not exist in the working tree anymore.' + - '
The "index" holds a snapshot of the content of the working tree, and it is this snapshot that is taken as the contents of the next commit. Thus after making any changes to the working directory, and before running the commit command, you must use the add command to add any new or modified files to the index.' + - '
This command can be performed multiple times before a commit. It only adds the content of the specified file(s) at the time the add command is run; if you want subsequent changes included in the next commit, then you must run git add again to add the new content to the index.' + - '
The git status command can be used to obtain a summary of which files have changes that are staged for the next commit.' + - '
The git add command will not add ignored files by default. If any ignored files were explicitly specified on the command line, git add will fail with a list of ignored files. Ignored files reached by directory recursion or filename globbing performed by Git (quote your globs before the shell) will be silently ignored. The git add command can be used to add ignored files with the -f (force) option.' + - '
Please see git-commit(1) for alternative ways to add content to a commit.', - params: [ - { - name: 'filepattern', - type: { name: 'array', subtype: 'string' }, - description: 'Files to add', - manual: 'Fileglobs (e.g. *.c) can be given to add all matching files. Also a leading directory name (e.g. dir to add dir/file1 and dir/file2) can be given to add all files in the directory, recursively.' - }, - { - group: 'Common Options', - params: [ - { - name: 'all', - short: 'a', - type: 'boolean', - description: 'All (unignored) files', - manual: 'That means that it will find new files as well as staging modified content and removing files that are no longer in the working tree.' - }, - { - name: 'verbose', - short: 'v', - type: 'boolean', - description: 'Verbose output' - }, - { - name: 'dry-run', - short: 'n', - type: 'boolean', - description: 'Dry run', - manual: 'Don\'t actually add the file(s), just show if they exist and/or will be ignored.' - }, - { - name: 'force', - short: 'f', - type: 'boolean', - description: 'Allow ignored files', - manual: 'Allow adding otherwise ignored files.' - } - ] - }, - { - group: 'Advanced Options', - params: [ - { - name: 'update', - short: 'u', - type: 'boolean', - description: 'Match only files already added', - manual: 'That means that it will never stage new files, but that it will stage modified new contents of tracked files and that it will remove files from the index if the corresponding files in the working tree have been removed.
If no is given, default to "."; in other words, update all tracked files in the current directory and its subdirectories.' - }, - { - name: 'refresh', - type: 'boolean', - description: 'Refresh only (don\'t add)', - manual: 'Don\'t add the file(s), but only refresh their stat() information in the index.' - }, - { - name: 'ignore-errors', - type: 'boolean', - description: 'Ignore errors', - manual: 'If some files could not be added because of errors indexing them, do not abort the operation, but continue adding the others. The command shall still exit with non-zero status.' - }, - { - name: 'ignore-missing', - type: 'boolean', - description: 'Ignore missing', - manual: 'By using this option the user can check if any of the given files would be ignored, no matter if they are already present in the work tree or not. This option can only be used together with --dry-run.' - } - ] - } - ], - exec: function(args, context) { - return 'This is only a demo of UI generation.'; - } - }, - { - // 'git commit' command - item: 'command', - name: 'git commit', - description: 'Record changes to the repository', - manual: 'Stores the current contents of the index in a new commit along with a log message from the user describing the changes.' + - '
The content to be added can be specified in several ways:' + - '
1. by using git add to incrementally "add" changes to the index before using the commit command (Note: even modified files must be "added");' + - '
2. by using git rm to remove files from the working tree and the index, again before using the commit command;' + - '
3. by listing files as arguments to the commit command, in which case the commit will ignore changes staged in the index, and instead record the current content of the listed files (which must already be known to git);' + - '
4. by using the -a switch with the commit command to automatically "add" changes from all known files (i.e. all files that are already listed in the index) and to automatically "rm" files in the index that have been removed from the working tree, and then perform the actual commit;' + - '
5. by using the --interactive switch with the commit command to decide one by one which files should be part of the commit, before finalizing the operation. Currently, this is done by invoking git add --interactive.' + - '
The --dry-run option can be used to obtain a summary of what is included by any of the above for the next commit by giving the same set of parameters (options and paths).' + - '
If you make a commit and then find a mistake immediately after that, you can recover from it with git reset.', - params: [ - { - name: 'file', - short: 'F', - type: { name: 'array', subtype: 'existingFile' }, - description: 'Files to commit', - manual: 'When files are given on the command line, the command commits the contents of the named files, without recording the changes already staged. The contents of these files are also staged for the next commit on top of what have been staged before.' - }, - { - group: 'Common Options', - params: [ - { - name: 'all', - short: 'a', - type: 'boolean', - description: 'All (unignored) files', - manual: 'Tell the command to automatically stage files that have been modified and deleted, but new files you have not told git about are not affected.' - }, - { - name: 'message', - short: 'm', - type: 'string', - description: 'Commit message', - manual: 'Use the given message as the commit message.' - }, - { - name: 'signoff', - short: 's', - type: 'string', - description: 'Signed off by', - manual: 'Add Signed-off-by line by the committer at the end of the commit log message.' - } - ] - }, - { - group: 'Advanced Options', - params: [ - { - name: 'author', - type: 'string', - description: 'Override the author', - manual: 'Specify an explicit author using the standard A U Thor format. Otherwise is assumed to be a pattern and is used to search for an existing commit by that author (i.e. rev-list --all -i --author=); the commit author is then copied from the first such commit found.' - }, - { - name: 'date', - type: 'string', // Make this of date type - description: 'Override the date', - manual: 'Override the author date used in the commit.' - }, - { - name: 'amend', - type: 'boolean', - description: 'Amend tip', - manual: 'Used to amend the tip of the current branch. Prepare the tree object you would want to replace the latest commit as usual (this includes the usual -i/-o and explicit paths), and the commit log editor is seeded with the commit message from the tip of the current branch. The commit you create replaces the current tip -- if it was a merge, it will have the parents of the current tip as parents -- so the current top commit is discarded.' - }, - { - name: 'verbose', - short: 'v', - type: 'boolean', - description: 'Verbose', - manual: 'Show unified diff between the HEAD commit and what would be committed at the bottom of the commit message template. Note that this diff output doesn\'t have its lines prefixed with #.' - }, - { - name: 'quiet', - short: 'q', - type: 'boolean', - description: 'Quiet', - manual: 'Suppress commit summary message.' - }, - { - name: 'dry-run', - type: 'boolean', - description: 'Dry run', - manual: 'Do not create a commit, but show a list of paths that are to be committed, paths with local changes that will be left uncommitted and paths that are untracked.' - }, - { - name: 'untracked-files', - short: 'u', - type: { - name: 'selection', - data: [ 'no', 'normal', 'all' ] - }, - description: 'Show untracked files', - manual: 'The mode parameter is optional, and is used to specify the handling of untracked files. The possible options are: no - Show no untracked files.
normal Shows untracked files and directories
all Also shows individual files in untracked directories.' - } - ] - } - ], - exec: function(args, context) { - return 'This is only a demo of UI generation.'; - } - }, - { - item: 'command', - name: 'git remote', - description: 'Manage set of tracked repositories', - manual: 'Manage the set of repositories ("remotes") whose branches you track.' - }, - { - item: 'command', - name: 'git remote rename', - description: 'Rename a remote', - params: [ - { - name: 'old', - type: 'string' - }, - { - name: 'new', - type: 'string' - } - ], - exec: function(args, context) { - return 'This is only a demo of UI generation.'; - } - }, - { - item: 'command', - name: 'git remote add', - description: 'Add a remote', - params: [ - { - name: 'name', - type: 'string', - description: 'A short name for a remote repository' - }, - { - name: 'url', - type: 'string', - description: 'URL that declares where the remote repository is' - } - ], - exec: function(args, context) { - return 'This is only a demo of UI generation.'; - } - } -]; diff --git a/lib/gcli/commands/demo/hg.js b/lib/gcli/commands/demo/hg.js deleted file mode 100644 index 2a39a8d9..00000000 --- a/lib/gcli/commands/demo/hg.js +++ /dev/null @@ -1,81 +0,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. - */ - -'use strict'; - -var host = require('../../util/host'); - -exports.items = [ - { - // Top level 'hg' command - item: 'command', - name: 'hg', - description: 'Mercurial is a free, distributed source control management tool', - manual: 'Mercurial is a free, distributed source control management tool. It efficiently handles projects of any size and offers an easy and intuitive interface.' - }, - { - // Convert a list of patches to a DOM view - item: 'converter', - from: 'patches', - to: 'view', - exec: function(patches, context) { - return { - html: - '\n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - '
${patch.name}
${patch.comment}
\n' + - ' goto\n' + - '
\n', - data: { - patches: patches, - onclick: context.update, - ondblclick: context.updateExec - } - }; - } - }, - { - // 'hg qseries' command - name: 'hg qseries', - description: 'Print the entire series file', - params: [ ], - returnType: 'patches', - exec: function(args, context) { - var spawnSpec = { - cmd: '/usr/local/bin/hg', - args: [ 'qseries' ], - cwd: context.shell.cwd, - env: context.shell.env - }; - - return host.spawn(context, spawnSpec).then(function(output) { - return output.split('\n').map(function(line) { - return { - name: line.split(':', 1)[0], - comment: line.substring(name.length + 2) - }; - }); - }); - } - } -]; diff --git a/lib/gcli/commands/demo/theme.js b/lib/gcli/commands/demo/theme.js deleted file mode 100644 index 46b159a8..00000000 --- a/lib/gcli/commands/demo/theme.js +++ /dev/null @@ -1,96 +0,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. - */ - -'use strict'; - -exports.items = [ - { - item: 'type', - name: 'theme', - parent: 'selection', - data: [ 'dark', 'light' ] - }, - { - item: 'command', - name: 'theme', - description: 'Change themes', - params: [ - { - name: 'theme', - type: 'theme', - description: 'The theme to use' - }, - { - name: 'show', - type: 'boolean', - description: 'Display a preview of the current theme', - hidden: true, - option: true - } - ], - exec: function(args, context) { - if (args.show) { - return context.typedData('theme-preview', args); - } - else { - return context.typedData('theme-change', args); - } - } - }, - { - item: 'converter', - from: 'theme-change', - to: 'view', - exec: function(args, context) { - var body = context.document.body; - - // Remove existing themes. This is very dependent on how themes are - // setup. This code will probably require local customization - exports.items[0].data.forEach(function(theme) { - body.classList.remove(theme); - }); - body.classList.add(args.theme); - - return { - html: '
Set theme to ${theme}
', - data: args, - options: { allowEval: true, stack: 'theme.html#change' } - }; - } - }, - { - item: 'converter', - from: 'theme-preview', - to: 'view', - exec: function(args, context) { - return { - html: - '
\n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - '
${className}Lorem ipsum dolor sit amet ↑ → ↓ ← ██████████
\n' + - '
', - data: args, - options: { allowEval: true, stack: 'theme.html#preview' } - }; - } - } -]; diff --git a/lib/gcli/commands/server/firefox.js b/lib/gcli/commands/server/firefox.js index b362a209..f7fc0ce0 100644 --- a/lib/gcli/commands/server/firefox.js +++ b/lib/gcli/commands/server/firefox.js @@ -136,14 +136,14 @@ function createCommonJsToJsTestFilter() { 'var TEST_URI = "data:text/html;charset=utf-8,

gcli-' + name + '

";\n' + '\n' + 'function test() {\n' + - ' return Task.spawn(function() {\n' + + ' return Task.spawn(*function() {\n' + ' let options = yield helpers.openTab(TEST_URI);\n' + ' yield helpers.openToolbar(options);\n' + - ' gcli.addItems(mockCommands.items);\n' + + ' options.requisition.system.addItems(mockCommands.items);\n' + '\n' + ' yield helpers.runTests(options, exports);\n' + '\n' + - ' gcli.removeItems(mockCommands.items);\n' + + ' options.requisition.system.removeItems(mockCommands.items);\n' + ' yield helpers.closeToolbar(options);\n' + ' yield helpers.closeTab(options);\n' + ' }).then(finish, helpers.handleError);\n' + diff --git a/lib/gcli/commands/server/server.js b/lib/gcli/commands/server/server.js index 746d5cdb..45494228 100644 --- a/lib/gcli/commands/server/server.js +++ b/lib/gcli/commands/server/server.js @@ -115,7 +115,6 @@ exports.items = [ }); }); - app.use('/lib', function(req, res) { var filename = main.gcliHome + '/lib' + url.parse(req.url).pathname; var webOverride = filename.replace(/\/lib\/gcli\//, '/web/gcli/'); @@ -193,7 +192,7 @@ var xhrsocket = { res.status(500).send(text); }; - return Promise.resolve(reply).then(onResolve).then(null, onReject); + return Promise.resolve(reply).then(onResolve).catch(onReject); }); }); } @@ -249,31 +248,40 @@ var websocket = { var remoter = new Remoter(requisition); remoter.addListener(function(name, data) { - console.log('EMIT ' + name + ' ' + debugStr(data, 30)); + // console.log('EMIT ' + name + ' ' + debugStr(data, 30)); socket.emit('event', { name: name, data: data }); }); Object.keys(remoter.exposed).forEach(function(command) { socket.on(command, function(request) { - console.log('SOCKET ' + command + ' ' + debugStr(request.data, 30)); + // Handle errors from exceptions an promise rejections + var onError = function(err) { + console.log('SOCKET ' + command + + '(' + debugStr(request.data, 30) + ') Exception'); + util.errorHandler(err); + + socket.emit('reply', { + id: request.id, + exception: '' + err + }); + }; try { var func = remoter.exposed[command]; var reply = func.call(remoter, request.data); Promise.resolve(reply).then(function(data) { + console.log('SOCKET ' + command + + '(' + debugStr(request.data, 30) + ') → ' + + debugStr(data, 20) + ')'); + socket.emit('reply', { id: request.id, reply: data }); - }); + }, onError); } catch (ex) { - console.trace(); - console.error(ex); - socket.emit('reply', { - id: request.id, - exception: ex.toString() - }); + onError(ex); } }); }); diff --git a/lib/gcli/connectors/connectors.js b/lib/gcli/connectors/connectors.js index cf543823..1578f9c5 100644 --- a/lib/gcli/connectors/connectors.js +++ b/lib/gcli/connectors/connectors.js @@ -138,14 +138,21 @@ Connectors.prototype.getAll = function() { }.bind(this)); }; +var defaultConnectorName; + /** - * Get access to a connector by name. If name is undefined then use the first - * registered connector as a default. + * Get access to a connector by name. If name is undefined then first try to + * use the same connector that we used last time, and if there was no last + * time, then just use the first registered connector as a default. */ Connectors.prototype.get = function(name) { if (name == null) { - name = Object.keys(this._registered)[0]; + name = (defaultConnectorName == null) ? + Object.keys(this._registered)[0] : + defaultConnectorName; } + + defaultConnectorName = name; return this._registered[name]; }; diff --git a/lib/gcli/connectors/direct.js b/lib/gcli/connectors/direct.js index 249637d3..2913628c 100644 --- a/lib/gcli/connectors/direct.js +++ b/lib/gcli/connectors/direct.js @@ -16,60 +16,12 @@ 'use strict'; -var system = require('../api').createSystem(); -var Commands = require('../commands/commands').Commands; -var Types = require('../types/types').Types; -var Requisition = require('../cli').Requisition; var Promise = require('../util/promise').Promise; - +var createSystem = require('../system').createSystem; +var Requisition = require('../cli').Requisition; var Remoter = require('./remoted').Remoter; var Connection = require('./connectors').Connection; -system.types = new Types(); -system.commands = new Commands(system.types); - -var items = [ - require('../types/delegate').items, - require('../types/selection').items, - - require('../types/array').items, - require('../types/boolean').items, - require('../types/command').items, - require('../types/date').items, - require('../types/file').items, - require('../types/javascript').items, - require('../types/node').items, - require('../types/number').items, - require('../types/resource').items, - require('../types/setting').items, - require('../types/string').items, - require('../types/union').items, - require('../types/url').items, - - require('../cli').items, - - require('../commands/clear').items, - require('../commands/connect').items, - require('../commands/context').items, - require('../commands/exec').items, - require('../commands/global').items, - require('../commands/help').items, - require('../commands/intro').items, - require('../commands/lang').items, - require('../commands/mocks').items, - require('../commands/preflist').items, - require('../commands/pref').items, - require('../commands/test').items, - require('../commands/demo/alert').items, - require('../commands/demo/echo').items, - require('../commands/demo/sleep').items - -].reduce(function(prev, curr) { return prev.concat(curr); }, []); - -system.addItems(items); - -var requisition = new Requisition(system); - exports.items = [ { // Communicate with a 'remote' server that isn't remote at all @@ -82,14 +34,21 @@ exports.items = [ } ]; -/** - * Direct connection is mostly for testing. Async is closer to what things like - * websocket will actually give us, but sync gives us clearer stack traces. - */ -var asyncCall = false; - function DirectConnection() { this._emit = this._emit.bind(this); + + // The items to use in our new command line + var items = [ + require('../items/basic').items, + require('../items/standard').items, + require('../items/demo').items, + ].reduce(function(prev, curr) { return prev.concat(curr); }, []); + + // This is the 'server' + var system = createSystem(); + system.addItems(items); + var requisition = new Requisition(system); + this.remoter = new Remoter(requisition); this.remoter.addListener(this._emit); } @@ -98,26 +57,10 @@ DirectConnection.prototype = Object.create(Connection.prototype); DirectConnection.prototype.call = function(command, data) { return new Promise(function(resolve, reject) { - if (asyncCall) { - setTimeout(function() { - this._call(resolve, reject, command, data); - }, 1); - } - else { - this._call(resolve, reject, command, data); - } - }.bind(this)); -}; - -DirectConnection.prototype._call = function(resolve, reject, command, data) { - try { var func = this.remoter.exposed[command]; var reply = func.call(this.remoter, data); resolve(reply); - } - catch (ex) { - reject(ex); - } + }.bind(this)); }; DirectConnection.prototype.disconnect = function() { diff --git a/lib/gcli/connectors/index.js b/lib/gcli/connectors/index.js index 5e353193..9f98e9e2 100644 --- a/lib/gcli/connectors/index.js +++ b/lib/gcli/connectors/index.js @@ -16,170 +16,55 @@ 'use strict'; -var api = require('../api'); -var Commands = require('../commands/commands').Commands; -var Types = require('../types/types').Types; +var createSystem = require('../system').createSystem; +var connectFront = require('../system').connectFront; +var GcliFront = require('./remoted').GcliFront; // Patch-up IE9 require('../util/legacy'); -/* - * GCLI is built from a number of components (called items) composed as - * required for each environment. - * When adding to or removing from this list, we should keep the basics in sync - * with the other environments. - * See: - * - lib/gcli/index.js: Generic basic set (without commands) - * - lib/gcli/demo.js: Adds demo commands to basic set for use in web demo - * - gcli.js: Add commands to basic set for use in Node command line - * - mozilla/gcli/index.js: From scratch listing for Firefox - * - lib/gcli/connectors/index.js: Client only items when executing remotely - * - lib/gcli/connectors/direct.js: Test items for connecting to in-process GCLI - */ -var items = [ - // First we need to add the local types which other types depend on - require('../types/delegate').items, - require('../types/selection').items, - require('../types/array').items, - - require('../types/boolean').items, - require('../types/command').items, - require('../types/date').items, - require('../types/file').items, - require('../types/javascript').items, - require('../types/node').items, - require('../types/number').items, - require('../types/resource').items, - require('../types/setting').items, - require('../types/string').items, - require('../types/union').items, - require('../types/url').items, - - require('../fields/fields').items, - require('../fields/delegate').items, - require('../fields/selection').items, - - require('../ui/intro').items, - require('../ui/focus').items, - - require('../converters/converters').items, - require('../converters/basic').items, - require('../converters/html').items, - require('../converters/terminal').items, - - require('../languages/command').items, - require('../languages/javascript').items, - - require('./direct').items, - // require('./rdp').items, // Firefox remote debug protocol - require('./websocket').items, - require('./xhr').items, - - require('../commands/context').items, - -].reduce(function(prev, curr) { return prev.concat(curr); }, []); - -/** - * These are the commands stored on the remote side that have converters which - * we'll need to present the data - */ -var requiredConverters = [ - require('../cli').items, - - require('../commands/clear').items, - require('../commands/connect').items, - require('../commands/exec').items, - require('../commands/global').items, - require('../commands/help').items, - require('../commands/intro').items, - require('../commands/lang').items, - require('../commands/preflist').items, - require('../commands/pref').items, - require('../commands/test').items, - -].reduce(function(prev, curr) { return prev.concat(curr); }, []) - .filter(function(item) { return item.item === 'converter'; }); - /** * Connect to a remote system and setup the commands/types/converters etc needed * to make it all work */ -exports.connect = function(options) { +exports.createSystem = function(options) { options = options || {}; - var system = api.createSystem(); - - // Ugly hack, to aid testing - exports.api = system; - - options.types = system.types = new Types(); - options.commands = system.commands = new Commands(system.types); + var system = createSystem(); + // The items that are always needed on the client + var items = [ + require('../items/basic').items, + require('../items/ui').items, + require('../items/remote').items, + // The context command makes no sense on the server + require('../commands/context').items, + ].reduce(function(prev, curr) { return prev.concat(curr); }, []); system.addItems(items); - system.addItems(requiredConverters); - - var connector = system.connectors.get(options.connector); - return connector.connect(options.url).then(function(connection) { - options.connection = connection; - connection.on('commandsChanged', function(specs) { - exports.addItems(system, specs, connection); - }); - return connection.call('specs').then(function(specs) { - exports.addItems(system, specs, connection); - return connection; - }); - }); -}; - -exports.addItems = function(gcli, specs, connection) { - exports.removeRemoteItems(gcli, connection); - var remoteItems = exports.addLocalFunctions(specs, connection); - gcli.addItems(remoteItems); -}; + // These are the commands stored on the remote side that have converters which + // we'll need to present the data. Ideally front.specs() would transfer these, + // that doesn't happen yet so we add them manually + var requiredConverters = [ + require('../cli').items, + require('../commands/clear').items, + require('../commands/connect').items, + require('../commands/exec').items, + require('../commands/global').items, + require('../commands/help').items, + require('../commands/intro').items, + require('../commands/lang').items, + require('../commands/preflist').items, + require('../commands/pref').items, + require('../commands/test').items, + ].reduce(function(prev, curr) { return prev.concat(curr); }, []) + .filter(function(item) { return item.item === 'converter'; }); + system.addItems(requiredConverters); -/** - * Take the data from the 'specs' command (or the 'commandsChanged' event) and - * add function to proxy the execution back over the connection - */ -exports.addLocalFunctions = function(specs, connection) { - // Inject an 'exec' function into the commands, and the connection into - // all the remote types - specs.forEach(function(commandSpec) { - // - commandSpec.connection = connection; - commandSpec.params.forEach(function(param) { - param.type.connection = connection; + var connector = system.connectors.get(options.method); + return GcliFront.create(connector, options.url).then(function(front) { + return connectFront(system, front).then(function() { + return system; }); - - if (!commandSpec.isParent) { - commandSpec.exec = function(args, context) { - var data = { - typed: (context.prefix ? context.prefix + ' ' : '') + context.typed - }; - - return connection.call('execute', data).then(function(reply) { - var typedData = context.typedData(reply.type, reply.data); - if (!reply.error) { - return typedData; - } - else { - throw typedData; - } - }); - }; - } - - commandSpec.isProxy = true; - }); - - return specs; -}; - -exports.removeRemoteItems = function(gcli, connection) { - gcli.commands.getAll().forEach(function(command) { - if (command.connection === connection) { - gcli.commands.remove(command); - } }); }; diff --git a/lib/gcli/connectors/rdp.js b/lib/gcli/connectors/rdp.js deleted file mode 100644 index cde7a8e1..00000000 --- a/lib/gcli/connectors/rdp.js +++ /dev/null @@ -1,141 +0,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. - */ - -'use strict'; - -var Cu = require('chrome').Cu; - -var debuggerSocketConnect = Cu.import('resource://gre/modules/devtools/dbg-client.jsm', {}).debuggerSocketConnect; -var DebuggerClient = Cu.import('resource://gre/modules/devtools/dbg-client.jsm', {}).DebuggerClient; - -var Promise = require('../util/promise').Promise; -var Connection = require('./connectors').Connection; - -/** - * What port should we use by default? - */ -Object.defineProperty(exports, 'defaultPort', { - get: function() { - var Services = Cu.import('resource://gre/modules/Services.jsm', {}).Services; - try { - return Services.prefs.getIntPref('devtools.debugger.chrome-debugging-port'); - } - catch (ex) { - console.error('Can\'t use default port from prefs. Using 9999'); - return 9999; - } - }, - enumerable: true -}); - -exports.items = [ - { - item: 'connector', - name: 'rdp', - - connect: function(url) { - return RdpConnection.create(url); - } - } -]; - -/** - * RdpConnection uses the Firefox Remote Debug Protocol - */ -function RdpConnection(url) { - throw new Error('Use RdpConnection.create'); -} - -/** - * Asynchronous construction - */ -RdpConnection.create = function(url) { - this.host = url; - this.port = undefined; // TODO: Split out the port number - - this.requests = {}; - this.nextRequestId = 0; - - this._emit = this._emit.bind(this); - - return new Promise(function(resolve, reject) { - this.transport = debuggerSocketConnect(this.host, this.port); - this.client = new DebuggerClient(this.transport); - this.client.connect(function() { - this.client.listTabs(function(response) { - this.actor = response.gcliActor; - resolve(); - }.bind(this)); - }.bind(this)); - }.bind(this)); -}; - -RdpConnection.prototype = Object.create(Connection.prototype); - -RdpConnection.prototype.call = function(command, data) { - return new Promise(function(resolve, reject) { - var request = { to: this.actor, type: command, data: data }; - - this.client.request(request, function(response) { - resolve(response.commandSpecs); - }); - }.bind(this)); -}; - -RdpConnection.prototype.disconnect = function() { - return new Promise(function(resolve, reject) { - this.client.close(function() { - resolve(); - }); - - delete this._emit; - }.bind(this)); -}; - - -/** - * A Request is a command typed at the client which lives until the command - * has finished executing on the server - */ -function Request(actor, typed, args) { - this.json = { - to: actor, - type: 'execute', - typed: typed, - args: args, - requestId: 'id-' + Request._nextRequestId++, - }; - - this.promise = new Promise(function(resolve, reject) { - this._resolve = resolve; - }.bind(this)); -} - -Request._nextRequestId = 0; - -/** - * Called by the connection when a remote command has finished executing - * @param error boolean indicating output state - * @param type the type of the returned data - * @param data the data itself - */ -Request.prototype.complete = function(error, type, data) { - this._resolve({ - error: error, - type: type, - data: data - }); -}; diff --git a/lib/gcli/connectors/remoted.js b/lib/gcli/connectors/remoted.js index 91e6900f..92cc3586 100644 --- a/lib/gcli/connectors/remoted.js +++ b/lib/gcli/connectors/remoted.js @@ -78,11 +78,16 @@ Remoter.prototype.removeListener = function(action) { Remoter.prototype.exposed = { /** * Retrieve a list of the remotely executable commands + * @param customProps Array of strings containing additional properties which, + * if specified in the command spec, will be included in the JSON. Normally we + * transfer only the properties required for GCLI to function. */ - specs: method(function() { - return this.requisition.system.commands.getCommandSpecs(); + specs: method(function(customProps) { + return this.requisition.system.commands.getCommandSpecs(customProps); }, { - request: {}, + request: { + customProps: Arg(0, "nullable:array:string") + }, response: RetVal("json") }), @@ -127,7 +132,7 @@ Remoter.prototype.exposed = { * - message: The message to display to the user * - predictions: An array of suggested values for the given parameter */ - typeparse: method(function(typed, param) { + parseType: method(function(typed, param) { return this.requisition.update(typed).then(function() { var assignment = this.requisition.getAssignment(param); @@ -151,14 +156,14 @@ Remoter.prototype.exposed = { * Get the incremented value of some type * @return a promise of a string containing the new argument text */ - typeincrement: method(function(typed, param) { + incrementType: method(function(typed, param) { return this.requisition.update(typed).then(function() { var assignment = this.requisition.getAssignment(param); return this.requisition.increment(assignment).then(function() { var arg = assignment.arg; return arg == null ? undefined : arg.text; }); - }); + }.bind(this)); }, { request: { typed: Arg(0, "string"), // The command string @@ -168,16 +173,16 @@ Remoter.prototype.exposed = { }), /** - * See typeincrement + * See incrementType */ - typedecrement: method(function(typed, param) { + decrementType: method(function(typed, param) { return this.requisition.update(typed).then(function() { var assignment = this.requisition.getAssignment(param); return this.requisition.decrement(assignment).then(function() { var arg = assignment.arg; return arg == null ? undefined : arg.text; }); - }); + }.bind(this)); }, { request: { typed: Arg(0, "string"), // The command string @@ -187,38 +192,37 @@ Remoter.prototype.exposed = { }), /** - * Perform a lookup on a selection type to get the allowed values + * Call type.lookup() on a selection type to get the allowed values */ - selectioninfo: method(function(commandName, paramName, action) { - var command = this.requisition.system.commands.get(commandName); - if (command == null) { - throw new Error('No command called \'' + commandName + '\''); - } + getSelectionLookup: method(function(commandName, paramName) { + var type = getType(this.requisition, commandName, paramName); - var type; - command.params.forEach(function(param) { - if (param.name === paramName) { - type = param.type; - } + var context = this.requisition.executionContext; + return type.lookup(context).map(function(info) { + // lookup returns an array of objects with name/value properties and + // the values might not be JSONable, so remove them + return { name: info.name }; }); - if (type == null) { - throw new Error('No parameter called \'' + paramName + '\' in \'' + - commandName + '\''); - } + }, { + request: { + commandName: Arg(0, "string"), // The command containing the parameter in question + paramName: Arg(1, "string"), // The name of the parameter + }, + response: RetVal("json") + }), - switch (action) { - case 'lookup': - return type.lookup(this.requisition.executionContext); - case 'data': - return type.data(this.requisition.executionContext); - default: - throw new Error('Action must be either \'lookup\' or \'data\''); - } + /** + * Call type.data() on a selection type to get the allowed values + */ + getSelectionData: method(function(commandName, paramName) { + var type = getType(this.requisition, commandName, paramName); + + var context = this.requisition.executionContext; + return type.data(context); }, { request: { commandName: Arg(0, "string"), // The command containing the parameter in question - paramName: Arg(1, "string"), // The name of the parameter - action: Arg(2, "string") // 'lookup' or 'data' depending on the function to call + paramName: Arg(1, "string"), // The name of the parameter }, response: RetVal("json") }), @@ -243,7 +247,7 @@ Remoter.prototype.exposed = { /** * Examine the filesystem for file matches */ - parsefile: method(function(typed, filetype, existing, matches) { + parseFile: method(function(typed, filetype, existing, matches) { var options = { filetype: filetype, existing: existing, @@ -273,3 +277,141 @@ Remoter.prototype.exposed = { response: RetVal("json") }) }; + +/** + * Helper for #getSelectionLookup and #getSelectionData that finds a type + * instance given a commandName and paramName + */ +function getType(requisition, commandName, paramName) { + var command = requisition.system.commands.get(commandName); + if (command == null) { + throw new Error('No command called \'' + commandName + '\''); + } + + var type; + command.params.forEach(function(param) { + if (param.name === paramName) { + type = param.type; + } + }); + + if (type == null) { + throw new Error('No parameter called \'' + paramName + '\' in \'' + + commandName + '\''); + } + + return type; +} + + + +/** + * Asynchronous construction. Use GcliFront(); + * @private + */ +function GcliFront() { + throw new Error('Use GcliFront.create().then(front => ...)'); +} + +/** + * + */ +GcliFront.create = function(connector, url) { + return connector.connect(url).then(function(connection) { + var front = Object.create(GcliFront.prototype); + return front._init(connection); + }); +}; + +/** + * Asynchronous construction. Use GcliFront(); + * @private + */ +GcliFront.prototype._init = function(connection) { + this.connection = connection; + return this; +}; + +GcliFront.prototype.on = function(eventName, action) { + this.connection.on(eventName, action); +}; + +GcliFront.prototype.off = function(eventName, action) { + this.connection.off(eventName, action); +}; + + +GcliFront.prototype.specs = function() { + var data = { + }; + return this.connection.call('specs', data); +}; + +GcliFront.prototype.execute = function(typed) { + var data = { + typed: typed + }; + return this.connection.call('execute', data); +}; + +GcliFront.prototype.parseFile = function(typed, filetype, existing, matches) { + var data = { + typed: typed, + filetype: filetype, + existing: existing, + matches: matches + }; + return this.connection.call('parseFile', data); +}; + +GcliFront.prototype.parseType = function(typed, param) { + var data = { + typed: typed, + param: param + }; + return this.connection.call('parseType', data); +}; + +GcliFront.prototype.incrementType = function(typed, param) { + var data = { + typed: typed, + param: param + }; + return this.connection.call('incrementType', data); +}; + +GcliFront.prototype.decrementType = function(typed, param) { + var data = { + typed: typed, + param: param + }; + return this.connection.call('decrementType', data); +}; + +GcliFront.prototype.getSelectionLookup = function(commandName, paramName) { + var data = { + commandName: commandName, + paramName: paramName + }; + return this.connection.call('getSelectionLookup', data); +}; + +GcliFront.prototype.getSelectionData = function(commandName, paramName) { + var data = { + commandName: commandName, + paramName: paramName + }; + return this.connection.call('getSelectionData', data); +}; + +GcliFront.prototype.system = function(cmd, args, cwd, env) { + var data = { + cmd: cmd, + args: args, + cwd: cwd, + env: env + }; + return this.connection.call('system', data); +}; + +exports.GcliFront = GcliFront; diff --git a/lib/gcli/connectors/xhr.js b/lib/gcli/connectors/xhr.js index 78c37265..2926940e 100644 --- a/lib/gcli/connectors/xhr.js +++ b/lib/gcli/connectors/xhr.js @@ -49,7 +49,7 @@ XhrConnection.prototype = Object.create(Connection.prototype); * See server.js:remoted for details on the remoted functions * @param command The name of the exposed feature. * @param data The block of data to pass to the exposed feature - * @return A promise of the data returned by the remote feature + * @return Promise of the data returned by the remote feature */ XhrConnection.prototype.call = function(command, data) { return new Promise(function(resolve, reject) { diff --git a/lib/gcli/converters/basic.js b/lib/gcli/converters/basic.js index fdb41d4c..2efd9abb 100644 --- a/lib/gcli/converters/basic.js +++ b/lib/gcli/converters/basic.js @@ -54,6 +54,14 @@ exports.items = [ return util.createElement(conversionContext.document, 'span'); } }, + { + item: 'converter', + from: 'json', + to: 'dom', + exec: function(json, conversionContext) { + return nodeFromDataToString(JSON.stringify(json), conversionContext); + } + }, { item: 'converter', from: 'number', @@ -71,5 +79,13 @@ exports.items = [ from: 'undefined', to: 'string', exec: function(data) { return ''; } + }, + { + item: 'converter', + from: 'json', + to: 'string', + exec: function(json, conversionContext) { + return JSON.stringify(json); + } } ]; diff --git a/lib/gcli/converters/converters.js b/lib/gcli/converters/converters.js index 38b2bdc6..545755a4 100644 --- a/lib/gcli/converters/converters.js +++ b/lib/gcli/converters/converters.js @@ -208,6 +208,15 @@ Converters.prototype.get = function(from, to) { return converter; }; +/** + * Get all the registered converters. Most for debugging + */ +Converters.prototype.getAll = function() { + return Object.keys(this._registered.from).map(function(name) { + return this._registered.from[name]; + }.bind(this)); +}; + /** * Helper for get to pick the best fallback converter */ diff --git a/lib/gcli/demo.js b/lib/gcli/demo.js deleted file mode 100644 index 62a8e230..00000000 --- a/lib/gcli/demo.js +++ /dev/null @@ -1,58 +0,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. - */ - -'use strict'; - -/* - * GCLI is built from a number of components (called items) composed as - * required for each environment. - * When adding to or removing from this list, we should keep the basics in sync - * with the other environments. - * See: - * - lib/gcli/index.js: Generic basic set (without commands) - * - lib/gcli/demo.js: Adds demo commands to basic set for use in web demo - * - gcli.js: Add commands to basic set for use in Node command line - * - mozilla/gcli/index.js: From scratch listing for Firefox - * - lib/gcli/connectors/index.js: Client only items when executing remotely - * - lib/gcli/connectors/direct.js: Test items for connecting to in-process GCLI - */ -exports.items = [ - require('./cli').items, - require('./commands/clear').items, - require('./commands/connect').items, - require('./commands/context').items, - require('./commands/exec').items, - require('./commands/global').items, - require('./commands/help').items, - require('./commands/intro').items, - require('./commands/lang').items, - require('./commands/mocks').items, - require('./commands/pref').items, - require('./commands/preflist').items, - require('./commands/test').items, - - require('./commands/demo/alert').items, - require('./commands/demo/bugs').items, - require('./commands/demo/demo').items, - require('./commands/demo/echo').items, - require('./commands/demo/edit').items, - // require('./commands/demo/git').items, - // require('./commands/demo/hg').items, - require('./commands/demo/sleep').items, - require('./commands/demo/theme').items, - - // Exclude Node commands on web -].reduce(function(prev, curr) { return prev.concat(curr); }, []); diff --git a/lib/gcli/fields/fields.js b/lib/gcli/fields/fields.js index d8897b27..c9718473 100644 --- a/lib/gcli/fields/fields.js +++ b/lib/gcli/fields/fields.js @@ -199,6 +199,13 @@ Fields.prototype.get = function(type, options) { return new FieldConstructor(type, options); }; +/** + * Get all the registered fields. Most for debugging + */ +Fields.prototype.getAll = function() { + return this._fieldCtors.slice(); +}; + exports.Fields = Fields; /** diff --git a/lib/gcli/fields/selection.js b/lib/gcli/fields/selection.js index 1aa45f6e..d3e7597b 100644 --- a/lib/gcli/fields/selection.js +++ b/lib/gcli/fields/selection.js @@ -75,7 +75,7 @@ SelectionField.prototype.setConversion = function(conversion) { prediction; }, this); this.menu.show(items, conversion.arg.text); - }.bind(this), util.errorHandler); + }.bind(this)).catch(util.errorHandler); }; SelectionField.prototype.itemClicked = function(ev) { @@ -85,7 +85,7 @@ SelectionField.prototype.itemClicked = function(ev) { this.type.parse(arg, context).then(function(conversion) { this.onFieldChange({ conversion: conversion }); this.setMessage(conversion.message); - }.bind(this)).then(null, util.errorHandler); + }.bind(this)).catch(util.errorHandler); }; SelectionField.prototype.getConversion = function() { diff --git a/lib/gcli/index.js b/lib/gcli/index.js index e34c22a2..57e4e7d6 100644 --- a/lib/gcli/index.js +++ b/lib/gcli/index.js @@ -16,92 +16,44 @@ 'use strict'; -var api = require('./api'); -var Terminal = require('./ui/terminal').Terminal; - -// Patch-up old browsers -require('./util/legacy'); - /* - * GCLI is built from a number of components (called items) composed as - * required for each environment. - * When adding to or removing from this list, we should keep the basics in sync - * with the other environments. - * See: - * - lib/gcli/index.js: Generic basic set (without commands) - * - lib/gcli/demo.js: Adds demo commands to basic set for use in web demo - * - gcli.js: Add commands to basic set for use in Node command line - * - mozilla/gcli/index.js: From scratch listing for Firefox - * - lib/gcli/connectors/index.js: Client only items when executing remotely - * - lib/gcli/connectors/direct.js: Test items for connecting to in-process GCLI + * The intent of this module is to pull together all the parts needed to start + * a command line to make it easier to get started. + * + * Basic usage is like this: + * + * var gcli = require('gcli/index'); + * + * // A system is a set of commands/types/etc that the command line uses + * var system = gcli.createSystem(); + * system.addItems(gcli.items); + * system.addItems(gcli.commandItems); + * system.addItems([ + * // Your own commands go here + * ]); + * + * // Create the UI + * gcli.createTerminal(system).then(function(terminal) { + * // Take any actions when the command line starts for example + * terminal.language.showIntro(); + * }); */ -exports.items = [ - require('./types/delegate').items, - require('./types/selection').items, - require('./types/array').items, - - require('./types/boolean').items, - require('./types/command').items, - require('./types/date').items, - require('./types/file').items, - require('./types/javascript').items, - require('./types/node').items, - require('./types/number').items, - require('./types/resource').items, - require('./types/setting').items, - require('./types/string').items, - require('./types/union').items, - require('./types/url').items, - - require('./fields/fields').items, - require('./fields/delegate').items, - require('./fields/selection').items, - require('./ui/focus').items, - require('./ui/intro').items, - - require('./converters/converters').items, - require('./converters/basic').items, - require('./converters/html').items, - require('./converters/terminal').items, - - require('./languages/command').items, - require('./languages/javascript').items, - - // require('./connectors/direct').items, // Loopback for testing only - // require('./connectors/rdp').items, // Firefox remote debug protocol - require('./connectors/websocket').items, - require('./connectors/xhr').items, - - // No commands in the basic set -].reduce(function(prev, curr) { return prev.concat(curr); }, []); - -var system = api.createSystem(); +// Patch-up old browsers +require('./util/legacy'); -// Export the system API by adding it to our exports -Object.keys(system).forEach(function(key) { - exports[key] = system[key]; -}); +exports.createSystem = require('./system').createSystem; -system.addItems(exports.items); +var Terminal = require('./ui/terminal').Terminal; +exports.createTerminal = Terminal.create.bind(Terminal); /** - * createTerminal() calls 'Terminal.create()' but returns an object which - * exposes a much restricted set of functions rather than all those exposed - * by Terminal. - * This allows for robust testing without exposing too many internals. - * @param options See Terminal.create() for a description of the available - * options. + * This is all the items we need for a basic GCLI (except for the commands) */ -exports.createTerminal = function(options) { - options = options || {}; - if (options.settings != null) { - system.settings.setDefaults(options.settings); - } +exports.items = [ + require('./items/basic').items, + require('./items/ui').items, + require('./items/remote').items, +].reduce(function(prev, curr) { return prev.concat(curr); }, []); - return Terminal.create(system, options).then(function(terminal) { - options.terminal = terminal; - terminal.language.showIntro(); - return terminal; - }); -}; +exports.commandItems = require('./items/standard').items; diff --git a/lib/gcli/items/basic.js b/lib/gcli/items/basic.js new file mode 100644 index 00000000..6bb660ea --- /dev/null +++ b/lib/gcli/items/basic.js @@ -0,0 +1,50 @@ +/* + * 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'; + +/** + * This is a list of the types and converters that will be needed in almost all + * command lines. The types are listed in 2 sets because delegate and selection + * are depended on by others, (e.g boolean depends on selection) and array is + * built into cli.js parsing somewhat. + * + * Keeping this module small helps reduce bringing in unwanted dependencies. + */ +exports.items = [ + require('../types/delegate').items, + require('../types/selection').items, + require('../types/array').items, + + require('../types/boolean').items, + require('../types/command').items, + require('../types/date').items, + require('../types/file').items, + require('../types/javascript').items, + require('../types/node').items, + require('../types/number').items, + require('../types/resource').items, + require('../types/setting').items, + require('../types/string').items, + require('../types/union').items, + require('../types/url').items, + + require('../converters/converters').items, + require('../converters/basic').items, + require('../converters/html').items, + require('../converters/terminal').items, + +].reduce(function(prev, curr) { return prev.concat(curr); }, []); diff --git a/lib/gcli/commands/demo/edit.js b/lib/gcli/items/demo.js similarity index 56% rename from lib/gcli/commands/demo/edit.js rename to lib/gcli/items/demo.js index 62bb0883..88d6f390 100644 --- a/lib/gcli/commands/demo/edit.js +++ b/lib/gcli/items/demo.js @@ -16,24 +16,15 @@ 'use strict'; +/** + * This is a list of demo commands. + * + * Keeping this module small helps reduce bringing in unwanted dependencies. + */ exports.items = [ - { - item: 'command', - name: 'edit', - description: 'Edit a file', - params: [ - { - name: 'resource', - type: { name: 'resource', include: 'text/css' }, - description: 'The resource to edit' - } - ], - returnType: 'html', - exec: function(args, context) { - return args.resource.loadContents().then(function(data) { - return '

This is just a demo

' + - ''; - }); - } - } -]; + require('../commands/demo/alert').items, + require('../commands/demo/demo').items, + require('../commands/demo/echo').items, + require('../commands/demo/sleep').items, + +].reduce(function(prev, curr) { return prev.concat(curr); }, []); diff --git a/lib/gcli/items/remote.js b/lib/gcli/items/remote.js new file mode 100644 index 00000000..402ae545 --- /dev/null +++ b/lib/gcli/items/remote.js @@ -0,0 +1,29 @@ +/* + * 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'; + +/** + * This is a list of the available connectors when linking to GCLIs together + * + * Keeping this module small helps reduce bringing in unwanted dependencies. + */ +exports.items = [ + require('../connectors/websocket').items, + require('../connectors/xhr').items, + require('../connectors/direct').items, // Generally for testing only + +].reduce(function(prev, curr) { return prev.concat(curr); }, []); diff --git a/lib/gcli/items/server.js b/lib/gcli/items/server.js new file mode 100644 index 00000000..1697e89b --- /dev/null +++ b/lib/gcli/items/server.js @@ -0,0 +1,31 @@ +/* + * 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'; + +/** + * This is a list of the commands designed to run in nodejs / io.js. + * + * Keeping this module small helps reduce bringing in unwanted dependencies. + */ +exports.items = [ + require('../commands/server/exit').items, + require('../commands/server/firefox').items, + require('../commands/server/orion').items, + require('../commands/server/server').items, + require('../commands/server/standard').items + +].reduce(function(prev, curr) { return prev.concat(curr); }, []); diff --git a/lib/gcli/items/standard.js b/lib/gcli/items/standard.js new file mode 100644 index 00000000..1781379a --- /dev/null +++ b/lib/gcli/items/standard.js @@ -0,0 +1,40 @@ +/* + * 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'; + +/** + * This is a list of the standard commands that are likely to be in most + * command lines. + * + * Keeping this module small helps reduce bringing in unwanted dependencies. + */ +exports.items = [ + require('../cli').items, + require('../commands/clear').items, + require('../commands/connect').items, + require('../commands/context').items, + require('../commands/exec').items, + require('../commands/global').items, + require('../commands/help').items, + require('../commands/intro').items, + require('../commands/lang').items, + require('../commands/mocks').items, + require('../commands/pref').items, + require('../commands/preflist').items, + require('../commands/test').items, + +].reduce(function(prev, curr) { return prev.concat(curr); }, []); diff --git a/lib/gcli/items/ui.js b/lib/gcli/items/ui.js new file mode 100644 index 00000000..374364b2 --- /dev/null +++ b/lib/gcli/items/ui.js @@ -0,0 +1,36 @@ +/* + * 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'; + +/** + * This is a list of the fields, settings and languages that we need to build + * a user interface. + * + * Keeping this module small helps reduce bringing in unwanted dependencies. + */ +exports.items = [ + require('../fields/fields').items, + require('../fields/delegate').items, + require('../fields/selection').items, + + require('../ui/focus').items, + require('../ui/intro').items, + + require('../languages/command').items, + require('../languages/javascript').items, + +].reduce(function(prev, curr) { return prev.concat(curr); }, []); diff --git a/lib/gcli/languages/command.js b/lib/gcli/languages/command.js index f423a8e4..e8ec20af 100644 --- a/lib/gcli/languages/command.js +++ b/lib/gcli/languages/command.js @@ -184,7 +184,7 @@ var commandLanguage = exports.commandLanguage = { var isNew = (this.assignment !== newAssignment); this.assignment = newAssignment; - this.terminal.updateCompletion(); + this.terminal.updateCompletion().catch(util.errorHandler); if (isNew) { this.updateHints(); @@ -286,7 +286,10 @@ var commandLanguage = exports.commandLanguage = { } this.terminal.history.add(input); - this.terminal.unsetChoice(); + this.terminal.unsetChoice().catch(util.errorHandler); + + this.terminal._previousValue = this.terminal.inputElement.value; + this.terminal.inputElement.value = ''; return this.requisition.exec().then(function() { this.textChanged(); @@ -496,7 +499,7 @@ var commandLanguage = exports.commandLanguage = { this.terminal.scrollToBottom(); data.throbEle.style.display = ev.output.completed ? 'none' : 'block'; }.bind(this)); - }.bind(this)).then(null, console.error); + }.bind(this)).catch(console.error); this.terminal.addElement(data.rowinEle); this.terminal.addElement(data.rowoutEle); diff --git a/lib/gcli/languages/languages.js b/lib/gcli/languages/languages.js index 49e0f1ec..3c5e8845 100644 --- a/lib/gcli/languages/languages.js +++ b/lib/gcli/languages/languages.js @@ -51,8 +51,9 @@ var baseLanguage = { }, handleTab: function() { - this.terminal.unsetChoice(); - return RESOLVED; + return this.terminal.unsetChoice().then(function() { + return RESOLVED; + }, util.errorHandler); }, handleInput: function(input) { @@ -62,8 +63,9 @@ var baseLanguage = { }.bind(this)); } - this.terminal.unsetChoice(); - return RESOLVED; + return this.terminal.unsetChoice().then(function() { + return RESOLVED; + }, util.errorHandler); }, handleReturn: function(input) { @@ -80,7 +82,7 @@ var baseLanguage = { this.focusManager.outputted(); - this.terminal.unsetChoice(); + this.terminal.unsetChoice().catch(util.errorHandler); this.terminal.inputElement.value = ''; }.bind(this)); }, diff --git a/lib/gcli/nls/strings.js b/lib/gcli/nls/strings.js index 96b7c1b7..c9b7d785 100644 --- a/lib/gcli/nls/strings.js +++ b/lib/gcli/nls/strings.js @@ -38,7 +38,7 @@ var i18n = { cliOptions: 'Available Options', // The error message when the user types a command that isn't registered - cliUnknownCommand: 'Invalid Command', + cliUnknownCommand2: 'Invalid Command: \'%1$S\'.', // A parameter should have a value, but doesn't cliIncompleteParam: 'Value required for \'%1$S\'.', diff --git a/lib/gcli/settings.js b/lib/gcli/settings.js index 9f23f833..42231042 100644 --- a/lib/gcli/settings.js +++ b/lib/gcli/settings.js @@ -46,7 +46,7 @@ Settings.prototype.setDefaults = function(newValues) { if (this._settingValues[name] === undefined) { this._settingValues[name] = newValues[name]; } - }); + }.bind(this)); }; /** @@ -67,7 +67,6 @@ Settings.prototype.getAll = function(filter) { /** * Add a new setting - * @return The new Setting object */ Settings.prototype.add = function(prefSpec) { var type = this._types.createType(prefSpec.type); diff --git a/lib/gcli/system.js b/lib/gcli/system.js new file mode 100644 index 00000000..c62f8d8a --- /dev/null +++ b/lib/gcli/system.js @@ -0,0 +1,323 @@ +/* + * 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'; + +var Promise = require('./util/promise').Promise; +var util = require('./util/util'); +var Commands = require('./commands/commands').Commands; +var Connectors = require('./connectors/connectors').Connectors; +var Converters = require('./converters/converters').Converters; +var Fields = require('./fields/fields').Fields; +var Languages = require('./languages/languages').Languages; +var Settings = require('./settings').Settings; +var Types = require('./types/types').Types; + +/** + * This is the heart of the API that we expose to the outside. + * @param options Object that customizes how the system acts. Valid properties: + * - commands, connectors, converters, fields, languages, settings, types: + * Custom configured manager objects for these item types + * - location: a system with a location will ignore commands that don't have a + * matching runAt property. This is principly for client/server setups where + * we import commands from the server to the client, so a system with + * `{ location: 'client' }` will silently ignore commands with + * `{ runAt: 'server' }`. Any system without a location will accept commands + * with any runAt property (including none). + */ +exports.createSystem = function(options) { + options = options || {}; + var location = options.location; + + // The plural/singular thing may make you want to scream, but it allows us + // to say components[getItemType(item)], so a lookup here (and below) saves + // multiple lookups in the middle of the code + var components = { + connector: options.connectors || new Connectors(), + converter: options.converters || new Converters(), + field: options.fields || new Fields(), + language: options.languages || new Languages(), + type: options.types || new Types() + }; + components.setting = new Settings(components.type); + components.command = new Commands(components.type, location); + + var getItemType = function(item) { + if (item.item) { + return item.item; + } + // Some items are registered using the constructor so we need to check + // the prototype for the the type of the item + return (item.prototype && item.prototype.item) ? + item.prototype.item : 'command'; + }; + + var addItem = function(item) { + try { + components[getItemType(item)].add(item); + } + catch (ex) { + if (item != null) { + console.error('While adding: ' + item.name); + } + throw ex; + } + }; + + var removeItem = function(item) { + components[getItemType(item)].remove(item); + }; + + /** + * loadableModules is a lookup of names to module loader functions (like + * the venerable 'require') to which we can pass a name and get back a + * JS object (or a promise of a JS object). This allows us to have custom + * loaders to get stuff from the filesystem etc. + */ + var loadableModules = {}; + + /** + * loadedModules is a lookup by name of the things returned by the functions + * in loadableModules so we can track what we need to unload / reload. + */ + var loadedModules = {}; + + var unloadModule = function(name) { + var existingModule = loadedModules[name]; + if (existingModule != null) { + existingModule.items.forEach(removeItem); + } + delete loadedModules[name]; + }; + + var loadModule = function(name) { + var existingModule = loadedModules[name]; + unloadModule(name); + + // And load the new items + try { + var loader = loadableModules[name]; + return Promise.resolve(loader(name)).then(function(newModule) { + if (existingModule === newModule) { + return; + } + + if (newModule == null) { + throw 'Module \'' + name + '\' not found'; + } + + if (newModule.items == null || typeof newModule.items.forEach !== 'function') { + console.log('Exported properties: ' + Object.keys(newModule).join(', ')); + throw 'Module \'' + name + '\' has no \'items\' array export'; + } + + newModule.items.forEach(addItem); + + loadedModules[name] = newModule; + }); + } + catch (ex) { + console.error('Failed to load module ' + name + ': ' + ex); + console.error(ex.stack); + } + }; + + var pendingChanges = false; + + var system = { + addItems: function(items) { + items.forEach(addItem); + }, + + removeItems: function(items) { + items.forEach(removeItem); + }, + + addItemsByModule: function(names, options) { + options = options || {}; + if (typeof names === 'string') { + names = [ names ]; + } + names.forEach(function(name) { + if (options.loader == null) { + options.loader = function(name) { + return require(name); + }; + } + loadableModules[name] = options.loader; + + if (options.delayedLoad) { + pendingChanges = true; + } + else { + loadModule(name).catch(console.error); + } + }); + }, + + removeItemsByModule: function(name) { + delete loadableModules[name]; + unloadModule(name); + }, + + load: function() { + if (!pendingChanges) { + return Promise.resolve(); + } + + // clone loadedModules, so we can remove what is left at the end + var modules = Object.keys(loadedModules).map(function(name) { + return loadedModules[name]; + }); + + var promises = Object.keys(loadableModules).map(function(name) { + delete modules[name]; + return loadModule(name); + }); + + Object.keys(modules).forEach(unloadModule); + pendingChanges = false; + + return Promise.all(promises); + }, + + toString: function() { + return 'System [' + + 'commands:' + components.command.getAll().length + ', ' + + 'connectors:' + components.connector.getAll().length + ', ' + + 'converters:' + components.converter.getAll().length + ', ' + + 'fields:' + components.field.getAll().length + ', ' + + 'settings:' + components.setting.getAll().length + ', ' + + 'types:' + components.type.getTypeNames().length + ']'; + } + }; + + Object.defineProperty(system, 'commands', { + get: function() { return components.command; }, + enumerable: true + }); + + Object.defineProperty(system, 'connectors', { + get: function() { return components.connector; }, + enumerable: true + }); + + Object.defineProperty(system, 'converters', { + get: function() { return components.converter; }, + enumerable: true + }); + + Object.defineProperty(system, 'fields', { + get: function() { return components.field; }, + enumerable: true + }); + + Object.defineProperty(system, 'languages', { + get: function() { return components.language; }, + enumerable: true + }); + + Object.defineProperty(system, 'settings', { + get: function() { return components.setting; }, + enumerable: true + }); + + Object.defineProperty(system, 'types', { + get: function() { return components.type; }, + enumerable: true + }); + + return system; +}; + +/** + * Connect a local system with another at the other end of a connector + * @param system System to which we're adding commands + * @param front Front which allows access to the remote system from which we + * import commands + * @param customProps Array of strings specifying additional properties defined + * on remote commands that should be considered part of the metadata for the + * commands imported into the local system + */ +exports.connectFront = function(system, front, customProps) { + front.on('commandsChanged', function(specs) { + syncItems(system, front, customProps).catch(util.errorHandler); + }); + + return syncItems(system, front, customProps); +}; + +/** + * Remove the items in this system that came from a previous sync action, and + * re-add them. See connectFront() for explanation of properties + */ +function syncItems(system, front, customProps) { + return front.specs(customProps).then(function(specs) { + // Go through all the commands removing any that are associated with the + // given front. The method of association is the hack in addLocalFunctions. + system.commands.getAll().forEach(function(command) { + if (command.front === front) { + system.commands.remove(command); + } + }); + + var remoteItems = addLocalFunctions(specs, front); + system.addItems(remoteItems); + + return system; + }); +}; + +/** + * Take the data from the 'specs' command (or the 'commandsChanged' event) and + * add function to proxy the execution back over the front + */ +function addLocalFunctions(specs, front) { + // Inject an 'exec' function into the commands, and the front into + // all the remote types + specs.forEach(function(commandSpec) { + // HACK: Tack the front to the command so we know how to remove it + // in syncItems() below + commandSpec.front = front; + + // TODO: syncItems() doesn't remove types, so do we need this? + commandSpec.params.forEach(function(param) { + if (typeof param.type !== 'string') { + param.type.front = front; + } + }); + + if (!commandSpec.isParent) { + commandSpec.exec = function(args, context) { + var typed = (context.prefix ? context.prefix + ' ' : '') + context.typed; + + return front.execute(typed).then(function(reply) { + var typedData = context.typedData(reply.type, reply.data); + if (!reply.error) { + return typedData; + } + else { + throw typedData; + } + }); + }; + } + + commandSpec.isProxy = true; + }); + + return specs; +} diff --git a/lib/gcli/test/index.js b/lib/gcli/test/index.js index 33d18359..b49387b2 100644 --- a/lib/gcli/test/index.js +++ b/lib/gcli/test/index.js @@ -28,9 +28,7 @@ require('./suite'); /** * Some tricks to make studying the command line state easier */ -var addDebugAids = exports.addDebugAids = function(options, terminal) { - var requisition = terminal.language.requisition; - +var addDebugAids = exports.addDebugAids = function(options) { window.createDebugCheck = function() { helpers._createDebugCheck(options).then(function() { // Don't inline this - chrome console suckage @@ -40,7 +38,7 @@ var addDebugAids = exports.addDebugAids = function(options, terminal) { window.summaryJson = function() { var args = [ 'Requisition: ' ]; - var summary = requisition._summaryJson; + var summary = options.terminal.language.requisition._summaryJson; Object.keys(summary).forEach(function(name) { args.push(' ' + name + '='); args.push(summary[name]); @@ -48,8 +46,8 @@ var addDebugAids = exports.addDebugAids = function(options, terminal) { console.log.apply(console, args); console.log('Focus: ' + - 'tooltip=', terminal.focusManager._shouldShowTooltip(), - 'output=', terminal.focusManager._shouldShowOutput()); + 'tooltip=', options.terminal.focusManager._shouldShowTooltip(), + 'output=', options.terminal.focusManager._shouldShowOutput()); }; document.addEventListener('keyup', function(ev) { @@ -71,19 +69,20 @@ var addDebugAids = exports.addDebugAids = function(options, terminal) { * - Runs the unit tests automatically on startup * - Registers a 'test' command to re-run the unit tests */ -exports.run = function(options) { - var requisition = options.terminal.language.requisition; - - options.window = window; - options.automator = createTerminalAutomator(options.terminal); - options.requisition = requisition; - options.isNode = false; - options.isFirefox = false; - options.isPhantomjs = (window.navigator.userAgent.indexOf('hantom') !== -1); - options.isRemote = (options.connection != null); - options.hideExec = true; +exports.run = function(terminal, isRemote) { + var options = { + terminal: terminal, + window: window, + automator: createTerminalAutomator(terminal), + requisition: terminal.language.requisition, + isNode: false, + isFirefox: false, + isPhantomjs: (window.navigator.userAgent.indexOf('hantom') !== -1), + isRemote: isRemote, + hideExec: true + }; - addDebugAids(options, options.terminal); + addDebugAids(options); // phantom-test.js does phantom.exit() on `document.complete = true` var closeIfPhantomJs = function() { @@ -94,7 +93,7 @@ exports.run = function(options) { // PhantomJS may tell us to tell the server to shutdown if (window.location.href.indexOf('shutdown=true') > 0) { - shutdownServer(requisition).then(closeIfPhantomJs, closeIfPhantomJs); + shutdownServer(terminal.language.requisition).then(closeIfPhantomJs, closeIfPhantomJs); return; } @@ -111,7 +110,7 @@ exports.run = function(options) { console.log(helpers.timingSummary); document.testStatus = examiner.status.name; - setMocks(options, false).then(closeIfPhantomJs).then(null, function(ex) { + setMocks(options, false).then(closeIfPhantomJs).catch(function(ex) { console.error(ex); closeIfPhantomJs(); }); diff --git a/lib/gcli/test/testCanon.js b/lib/gcli/test/testCanon.js index f223db76..938d3062 100644 --- a/lib/gcli/test/testCanon.js +++ b/lib/gcli/test/testCanon.js @@ -195,6 +195,9 @@ exports.testAltCommands = function(options) { { name: 'num', type: 'number' }, { name: 'opt', type: { name: 'selection', data: [ '1', '2', '3' ] } }, ], + customProp1: 'localValue', + customProp2: true, + customProp3: 42, exec: function(args, context) { return context.commandName + ':' + args.str + ':' + args.num + ':' + args.opt; @@ -211,6 +214,24 @@ exports.testAltCommands = function(options) { '],"isParent":false}]', 'JSON.stringify(commandSpecs)'); + var customProps = [ 'customProp1', 'customProp2', 'customProp3', ]; + var commandSpecs2 = altCommands.getCommandSpecs(customProps); + assert.is(JSON.stringify(commandSpecs2), + '[{' + + '"item":"command",' + + '"name":"tss",' + + '"params":[' + + '{"name":"str","type":"string"},' + + '{"name":"num","type":"number"},' + + '{"name":"opt","type":{"name":"selection","data":["1","2","3"]}}' + + '],' + + '"isParent":false,' + + '"customProp1":"localValue",' + + '"customProp2":true,' + + '"customProp3":42' + + '}]', + 'JSON.stringify(commandSpecs)'); + var remoter = function(args, context) { assert.is(context.commandName, 'tss', 'commandName is tss'); diff --git a/lib/gcli/test/testCli2.js b/lib/gcli/test/testCli2.js index 1a8ecaf6..3b242336 100644 --- a/lib/gcli/test/testCli2.js +++ b/lib/gcli/test/testCli2.js @@ -581,7 +581,7 @@ exports.testNestedCommand = function(options) { } }, { - skipIf: options.isPhantomjs, + skipIf: options.isPhantomjs, // PhantomJS gets predictions wrong setup: 'tsn x', check: { input: 'tsn x', diff --git a/lib/gcli/test/testCompletion1.js b/lib/gcli/test/testCompletion1.js index afcff226..1b1821f0 100644 --- a/lib/gcli/test/testCompletion1.js +++ b/lib/gcli/test/testCompletion1.js @@ -159,7 +159,7 @@ exports.testActivate = function(options) { } }, { - skipIf: options.isPhantomjs, + skipIf: options.isPhantomjs, // PhantomJS gets predictions wrong setup: 'tsg d', check: { hints: ' [options] -> ccc' diff --git a/lib/gcli/test/testExec.js b/lib/gcli/test/testExec.js index 30b0e263..018e2b16 100644 --- a/lib/gcli/test/testExec.js +++ b/lib/gcli/test/testExec.js @@ -399,7 +399,7 @@ exports.testExecNode = function(options) { return helpers.audit(options, [ { - skipIf: options.isNoDom, + skipIf: options.isNoDom || options.isRemote, setup: 'tse :root', check: { input: 'tse :root', diff --git a/lib/gcli/test/testFile.js b/lib/gcli/test/testFile.js index c00dc4bf..78a434fa 100644 --- a/lib/gcli/test/testFile.js +++ b/lib/gcli/test/testFile.js @@ -23,10 +23,7 @@ var local = false; exports.testBasic = function(options) { return helpers.audit(options, [ { - // These tests require us to be using node directly or to be in - // PhantomJS connected to an execute enabled node server or to be in - // firefox. - skipRemainingIf: options.isPhantomjs || options.isFirefox, + skipRemainingIf: options.isFirefox, // No file implementation in Firefox setup: 'tsfile open /', check: { input: 'tsfile open /', diff --git a/lib/gcli/test/testPref1.js b/lib/gcli/test/testPref1.js index b227a8a1..4082a301 100644 --- a/lib/gcli/test/testPref1.js +++ b/lib/gcli/test/testPref1.js @@ -135,6 +135,7 @@ exports.testPrefSetStatus = function(options) { } }, { + skipIf: options.isRemote, setup: 'pref set tempTBool 4', check: { typed: 'pref set tempTBool 4', diff --git a/lib/gcli/test/testRemoteWs.js b/lib/gcli/test/testRemoteWs.js index 2f5f15aa..b67c7240 100644 --- a/lib/gcli/test/testRemoteWs.js +++ b/lib/gcli/test/testRemoteWs.js @@ -59,8 +59,8 @@ exports.testRemoteWebsocket = function(options) { check: { args: { prefix: { - value: function(connection) { - assert.is(connection.prefix, 'remote', 'disconnecting remote'); + value: function(front) { + assert.is(front.prefix, 'remote', 'disconnecting remote'); } } } @@ -88,8 +88,8 @@ exports.testRemoteWebsocket = function(options) { check: { args: { prefix: { - value: function(connection) { - assert.is(connection.prefix, 'remote', 'disconnecting remote'); + value: function(front) { + assert.is(front.prefix, 'remote', 'disconnecting remote'); } } } @@ -442,8 +442,8 @@ exports.testRemoteWebsocket = function(options) { unassigned: [ ], args: { prefix: { - value: function(connection) { - assert.is(connection.prefix, 'remote', 'disconnecting remote'); + value: function(front) { + assert.is(front.prefix, 'remote', 'disconnecting remote'); }, arg: ' remote', status: 'VALID', diff --git a/lib/gcli/test/testRemoteXhr.js b/lib/gcli/test/testRemoteXhr.js index e0c6359b..1792be19 100644 --- a/lib/gcli/test/testRemoteXhr.js +++ b/lib/gcli/test/testRemoteXhr.js @@ -59,8 +59,8 @@ exports.testRemoteXhr = function(options) { check: { args: { prefix: { - value: function(connection) { - assert.is(connection.prefix, 'remote', 'disconnecting remote'); + value: function(front) { + assert.is(front.prefix, 'remote', 'disconnecting remote'); } } } @@ -88,8 +88,8 @@ exports.testRemoteXhr = function(options) { check: { args: { prefix: { - value: function(connection) { - assert.is(connection.prefix, 'remote', 'disconnecting remote'); + value: function(front) { + assert.is(front.prefix, 'remote', 'disconnecting remote'); } } } @@ -442,8 +442,8 @@ exports.testRemoteXhr = function(options) { unassigned: [ ], args: { prefix: { - value: function(connection) { - assert.is(connection.prefix, 'remote', 'disconnecting remote'); + value: function(front) { + assert.is(front.prefix, 'remote', 'disconnecting remote'); }, arg: ' remote', status: 'VALID', diff --git a/lib/gcli/test/testUnion.js b/lib/gcli/test/testUnion.js index 8a27a0e1..b5b7a802 100644 --- a/lib/gcli/test/testUnion.js +++ b/lib/gcli/test/testUnion.js @@ -102,7 +102,7 @@ exports.testDefault = function(options) { } }, { - skipIf: options.isPhantomjs, // Phantom goes weird with predictions + skipIf: options.isPhantomjs, // PhantomJS gets predictions wrong setup: 'unionc1 5', check: { input: 'unionc1 5', @@ -136,7 +136,7 @@ exports.testDefault = function(options) { } }, { - skipRemainingIf: options.isPhantomjs, + skipIf: options.isPhantomjs, // PhantomJS URL type is broken setup: 'unionc2 on', check: { input: 'unionc2 on', diff --git a/lib/gcli/test/testUrl.js b/lib/gcli/test/testUrl.js index 54edee36..5a332b80 100644 --- a/lib/gcli/test/testUrl.js +++ b/lib/gcli/test/testUrl.js @@ -22,7 +22,7 @@ var helpers = require('./helpers'); exports.testDefault = function(options) { return helpers.audit(options, [ { - skipRemainingIf: options.isPhantomjs, + skipRemainingIf: options.isPhantomjs, // PhantomJS URL type is broken setup: 'urlc', check: { input: 'urlc', diff --git a/lib/gcli/testharness/examiner.js b/lib/gcli/testharness/examiner.js index e6d51438..edabd3c6 100644 --- a/lib/gcli/testharness/examiner.js +++ b/lib/gcli/testharness/examiner.js @@ -379,6 +379,14 @@ Test.prototype.run = function(options) { * Object.toString could be a lot better */ function toString(err) { + if (err === null) { + return 'null'; + } + + if (err === undefined) { + return 'undefined'; + } + // Convert err to a string if (typeof err === 'string') { return err; diff --git a/lib/gcli/types/delegate.js b/lib/gcli/types/delegate.js index 50597f6b..1e9cb99f 100644 --- a/lib/gcli/types/delegate.js +++ b/lib/gcli/types/delegate.js @@ -29,14 +29,6 @@ exports.items = [ item: 'type', name: 'delegate', - constructor: function() { - if (typeof this.delegateType !== 'function' && - typeof this.delegateType !== 'string') { - throw new Error('Instances of DelegateType need typeSpec.delegateType' + - ' to be a function that returns a type'); - } - }, - getSpec: function(commandName, paramName) { return { name: 'delegate', @@ -47,9 +39,7 @@ exports.items = [ // 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'); - }, + delegateType: undefined, stringify: function(value, context) { return this.getType(context).then(function(delegated) { @@ -80,6 +70,10 @@ exports.items = [ }, getType: function(context) { + if (this.delegateType === undefined) { + return Promise.resolve(this.types.createType('blank')); + } + var type = this.delegateType(context); if (typeof type.parse !== 'function') { type = this.types.createType(type); @@ -87,8 +81,8 @@ exports.items = [ return Promise.resolve(type); }, - // DelegateType is designed to be inherited from, so DelegateField needs a way - // to check if something works like a delegate without using 'name' + // 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, // Technically we perhaps should proxy this, except that properties are @@ -114,8 +108,7 @@ exports.items = [ }, parse: function(arg, context) { - var args = { typed: context.typed, param: this.param }; - return this.connection.call('typeparse', args).then(function(json) { + return this.front.parseType(context.typed, this.param).then(function(json) { var status = Status.fromString(json.status); var val = { stringified: arg.text }; return new Conversion(val, arg, status, json.message, json.predictions); @@ -123,15 +116,13 @@ exports.items = [ }, decrement: function(value, context) { - var args = { typed: context.typed, param: this.param }; - return this.connection.call('typedecrement', args).then(function(json) { + return this.front.decrementType(context.typed, this.param).then(function(json) { return { stringified: json.arg }; }); }, increment: function(value, context) { - var args = { typed: context.typed, param: this.param }; - return this.connection.call('typeincrement', args).then(function(json) { + return this.front.incrementType(context.typed, this.param).then(function(json) { return { stringified: json.arg }; }); } diff --git a/lib/gcli/types/selection.js b/lib/gcli/types/selection.js index 5ec3d38f..1ab4fea7 100644 --- a/lib/gcli/types/selection.js +++ b/lib/gcli/types/selection.js @@ -126,19 +126,11 @@ SelectionType.prototype.getLookup = function(context) { var reply; if (this.remoteLookup) { - reply = this.connection.call('selectioninfo', { - action: 'lookup', - commandName: this.commandName, - paramName: this.paramName - }); + reply = this.front.getSelectionLookup(this.commandName, this.paramName); reply = resolve(reply, context); } else if (this.remoteData) { - reply = this.connection.call('selectioninfo', { - action: 'data', - commandName: this.commandName, - paramName: this.paramName - }); + reply = this.front.getSelectionData(this.commandName, this.paramName); reply = resolve(reply, context).then(this._dataToLookup); } else if (typeof this.lookup === 'function') { @@ -227,7 +219,7 @@ exports.findPredictions = function(arg, lookup) { } // Exact hidden matches. If 'hidden: true' then we only allow exact matches - // All the tests after here check that !option.value.hidden + // All the tests after here check that !isHidden(option) for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) { option = lookup[i]; if (option.name === arg.text) { @@ -238,7 +230,7 @@ exports.findPredictions = function(arg, lookup) { // 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 (option._gcliLowerName.indexOf(match) === 0 && !isHidden(option)) { if (predictions.indexOf(option) === -1) { predictions.push(option); } @@ -249,7 +241,7 @@ exports.findPredictions = function(arg, lookup) { 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 (option._gcliLowerName.indexOf(match) !== -1 && !isHidden(option)) { if (predictions.indexOf(option) === -1) { predictions.push(option); } @@ -261,7 +253,7 @@ exports.findPredictions = function(arg, lookup) { if (predictions.length === 0) { var names = []; lookup.forEach(function(opt) { - if (!opt.value.hidden) { + if (!isHidden(opt)) { names.push(opt.name); } }); @@ -306,11 +298,21 @@ exports.convertPredictions = function(arg, predictions) { Promise.resolve(predictions)); }; +/** + * Checking that an option is hidden involves messing in properties on the + * value right now (which isn't a good idea really) we really should be marking + * that on the option, so this encapsulates the problem + */ +function isHidden(option) { + return option.hidden === true || + (option.value != null && option.value.hidden); +} + SelectionType.prototype.getBlank = function(context) { var predictFunc = function(context2) { return Promise.resolve(this.getLookup(context2)).then(function(lookup) { return lookup.filter(function(option) { - return !option.value.hidden; + return !isHidden(option); }).slice(0, Conversion.maxPredictions - 1); }); }.bind(this); diff --git a/lib/gcli/types/setting.js b/lib/gcli/types/setting.js index 509b9514..26c6f406 100644 --- a/lib/gcli/types/setting.js +++ b/lib/gcli/types/setting.js @@ -25,12 +25,15 @@ exports.items = [ cacheable: true, lookup: function(context) { var settings = context.system.settings; + + // Lazily add a settings.onChange listener to clear the cache if (!this._registeredListener) { settings.onChange.add(function(ev) { this.clearCache(); }, this); this._registeredListener = true; } + return settings.getAll().map(function(setting) { return { name: setting.name, value: setting }; }); diff --git a/lib/gcli/types/types.js b/lib/gcli/types/types.js index e92d2626..5b672c71 100644 --- a/lib/gcli/types/types.js +++ b/lib/gcli/types/types.js @@ -946,8 +946,8 @@ function Type() { } /** - * Get a JSONable data structure that entirely describes this type - * @param commandName/paramName The names of the command and parameter which we + * Get a JSONable data structure that entirely describes this type. + * commandName and paramName are the names of the command and parameter which we * are remoting to help the server get back to the remoted action. */ Type.prototype.getSpec = function(commandName, paramName) { diff --git a/lib/gcli/ui/menu.js b/lib/gcli/ui/menu.js index fef02103..6ba00e76 100644 --- a/lib/gcli/ui/menu.js +++ b/lib/gcli/ui/menu.js @@ -256,7 +256,7 @@ function getHighlightingProxy(item, match, document) { } /** - * @return The current choice index + * @return {int} current choice index */ Menu.prototype.getChoiceIndex = function() { return this._choice == null ? 0 : this._choice; diff --git a/lib/gcli/ui/terminal.js b/lib/gcli/ui/terminal.js index 2e7629d2..8ddf687e 100644 --- a/lib/gcli/ui/terminal.js +++ b/lib/gcli/ui/terminal.js @@ -42,11 +42,12 @@ function Terminal() { /** * A wrapper to take care of the functions concerning an input element - * @param components Object that links to other UI components. GCLI provided: + * @param options Object that links to other UI components. GCLI provided: * - requisition * - document */ Terminal.create = function(system, options) { + options = options || {}; if (resourcesPromise == null) { resourcesPromise = Promise.all([ host.staticRequire(module, './terminal.css'), @@ -150,7 +151,7 @@ Terminal.prototype._init = function(system, options, terminalCss, terminalHtml) this.focusManager.addMonitoredElement(this.tooltipElement, 'tooltip'); this.focusManager.addMonitoredElement(this.inputElement, 'input'); - this.onInputChange.add(this.updateCompletion, this); + this.onInputChange.add(this._updateCompletionWithErrorHandler, this); host.script.onOutput.add(this.onOutput); @@ -185,7 +186,7 @@ Terminal.prototype.destroy = function() { this.field.onFieldChange.remove(this.fieldChanged, this); this.field.destroy(); - this.onInputChange.remove(this.updateCompletion, this); + this.onInputChange.remove(this._updateCompletionWithErrorHandler, this); // Remove the output elements so they free the event handers util.clearElement(this.displayElement); @@ -265,7 +266,7 @@ Terminal.prototype._updateLanguage = function(language) { } this.language.updateHints(); - this.updateCompletion(); + this.updateCompletion().catch(util.errorHandler); this.promptElement.innerHTML = this.language.prompt; }; @@ -392,7 +393,7 @@ Terminal.prototype.onKeyDown = function(ev) { * if something went wrong. */ Terminal.prototype.onKeyUp = function(ev) { - this.handleKeyUp(ev).then(null, util.errorHandler); + this.handleKeyUp(ev).catch(util.errorHandler); }; /** @@ -584,6 +585,14 @@ Terminal.prototype.updateCompletion = function() { }.bind(this)); }; +/** + * Call updateCompletion, but log if something is wrong. To be called by + * event handlers that can't react to rejected promises. + */ +Terminal.prototype._updateCompletionWithErrorHandler = function() { + this.updateCompletion().catch(util.errorHandler); +}; + /** * The terminal acts on UP/DOWN if there is a menu showing */ diff --git a/lib/gcli/util/host.js b/lib/gcli/util/host.js index 9e84437d..0012a4fd 100644 --- a/lib/gcli/util/host.js +++ b/lib/gcli/util/host.js @@ -75,7 +75,7 @@ exports.Highlighter = Highlighter; /** * Helper to execute an arbitrary OS-level command. * @param context From which we get the shell containing cwd and env - * @param execSpec Object containing some of the following properties: + * @param spawnSpec Object containing some of the following properties: * - cmd (string): The command to execute (required) * - args (string[]): The arguments to pass to the command (default: []) * - cwd (string): The current working directory diff --git a/lib/gcli/util/util.js b/lib/gcli/util/util.js index 800e416c..4e205f4b 100644 --- a/lib/gcli/util/util.js +++ b/lib/gcli/util/util.js @@ -275,7 +275,7 @@ exports.promiseEach = function(array, action, scope) { }; var reply = action.call(scope, array[index], index, array); - Promise.resolve(reply).then(onSuccess).then(null, onFailure); + Promise.resolve(reply).then(onSuccess).catch(onFailure); }; callNext(0); diff --git a/remote.html b/remote.html index 6c586514..987d105f 100644 --- a/remote.html +++ b/remote.html @@ -27,12 +27,11 @@ }); require([ 'gcli/index', 'gcli/test/index', 'gcli/connectors/index' ], function(gcli, test, cnx) { - var options = { connector: 'websocket' }; - cnx.connect(options).then(function() { - gcli.createTerminal(options).then(function() { - test.run(options); + cnx.createSystem({ connector: 'websocket' }).then(function(system) { + gcli.createTerminal(system).then(function(terminal) { + test.run(terminal, true); }); - }).then(null, console.error.bind(console)); + }).catch(console.error.bind(console)); }); diff --git a/web/gcli/types/fileparser.js b/web/gcli/types/fileparser.js index b8e6c2b8..969c92b1 100644 --- a/web/gcli/types/fileparser.js +++ b/web/gcli/types/fileparser.js @@ -18,6 +18,8 @@ var Promise = require('../util/promise').Promise; var Status = require('./types').Status; +var util = require('../util/util'); +var GcliFront = require('../connectors/remoted').GcliFront; /** * Helper for the parse() function from the file type. @@ -38,23 +40,19 @@ var Status = require('./types').Status; * and can contain a boolean 'complete' property */ exports.parse = function(context, typed, options) { - var data = { - typed: typed, - filetype: options.filetype, - existing: options.existing, - matches: options.matches == null ? undefined : options.matches.source - }; + var matches = options.matches == null ? undefined : options.matches.source; - var connectors = context.system.connectors; - return connectors.get().connect().then(function(connection) { - return connection.call('parsefile', data).then(function(reply) { + var connector = context.system.connectors.get(); + return GcliFront.create(connector).then(function(front) { + return front.parseFile(typed, options.filetype, + options.existing, matches).then(function(reply) { reply.status = Status.fromString(reply.status); if (reply.predictions != null) { reply.predictor = function() { return Promise.resolve(reply.predictions); }; } - connection.disconnect(); + front.connection.disconnect().catch(util.errorHandler); return reply; }); }); diff --git a/web/gcli/util/host.js b/web/gcli/util/host.js index fef711d8..d4d06483 100644 --- a/web/gcli/util/host.js +++ b/web/gcli/util/host.js @@ -19,6 +19,7 @@ var util = require('./util'); var Promise = require('../util/promise').Promise; +var GcliFront = require('../connectors/remoted').GcliFront; /** * Markup a web page to highlight a collection of elements @@ -70,22 +71,20 @@ exports.Highlighter = Highlighter; */ exports.spawn = function(context, spawnSpec) { // Make sure we're only sending strings across XHR + var cmd = '' + spawnSpec.cmd; var cleanArgs = (spawnSpec.args || []).map(function(arg) { return '' + arg; }); + var cwd = '' + spawnSpec.cwd; var cleanEnv = Object.keys(spawnSpec.env || {}).reduce(function(prev, key) { prev[key] = '' + spawnSpec.env[key]; return prev; }, {}); - return context.system.connectors.get().connect().then(function(connection) { - return connection.call('system', { - cmd: '' + spawnSpec.cmd, - args: cleanArgs, - cwd: '' + spawnSpec.cwd, - env: cleanEnv - }).then(function(reply) { - connection.disconnect(); + var connector = context.system.connectors.get(); + return GcliFront.create(connector).then(function(front) { + return front.system(cmd, cleanArgs, cwd, cleanEnv).then(function(reply) { + front.connection.disconnect().catch(util.errorHandler); return reply; }); });