toolkit/devtools/gcli/source/lib/gcli/languages/command.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /*
michael@0 2 * Copyright 2012, Mozilla Foundation and contributors
michael@0 3 *
michael@0 4 * Licensed under the Apache License, Version 2.0 (the "License");
michael@0 5 * you may not use this file except in compliance with the License.
michael@0 6 * You may obtain a copy of the License at
michael@0 7 *
michael@0 8 * http://www.apache.org/licenses/LICENSE-2.0
michael@0 9 *
michael@0 10 * Unless required by applicable law or agreed to in writing, software
michael@0 11 * distributed under the License is distributed on an "AS IS" BASIS,
michael@0 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
michael@0 13 * See the License for the specific language governing permissions and
michael@0 14 * limitations under the License.
michael@0 15 */
michael@0 16
michael@0 17 'use strict';
michael@0 18
michael@0 19 var util = require('../util/util');
michael@0 20 var promise = require('../util/promise');
michael@0 21 var domtemplate = require('../util/domtemplate');
michael@0 22 var host = require('../util/host');
michael@0 23
michael@0 24 var Status = require('../types/types').Status;
michael@0 25 var cli = require('../cli');
michael@0 26 var Requisition = require('../cli').Requisition;
michael@0 27 var CommandAssignment = require('../cli').CommandAssignment;
michael@0 28 var fields = require('../fields/fields');
michael@0 29 var intro = require('../ui/intro');
michael@0 30
michael@0 31 var RESOLVED = promise.resolve(true);
michael@0 32
michael@0 33 /**
michael@0 34 * Various ways in which we need to manipulate the caret/selection position.
michael@0 35 * A value of null means we're not expecting a change
michael@0 36 */
michael@0 37 var Caret = exports.Caret = {
michael@0 38 /**
michael@0 39 * We are expecting changes, but we don't need to move the cursor
michael@0 40 */
michael@0 41 NO_CHANGE: 0,
michael@0 42
michael@0 43 /**
michael@0 44 * We want the entire input area to be selected
michael@0 45 */
michael@0 46 SELECT_ALL: 1,
michael@0 47
michael@0 48 /**
michael@0 49 * The whole input has changed - push the cursor to the end
michael@0 50 */
michael@0 51 TO_END: 2,
michael@0 52
michael@0 53 /**
michael@0 54 * A part of the input has changed - push the cursor to the end of the
michael@0 55 * changed section
michael@0 56 */
michael@0 57 TO_ARG_END: 3
michael@0 58 };
michael@0 59
michael@0 60 /**
michael@0 61 * Shared promise for loading command.html
michael@0 62 */
michael@0 63 var commandHtmlPromise;
michael@0 64
michael@0 65 var commandLanguage = exports.commandLanguage = {
michael@0 66 // Language implementation for GCLI commands
michael@0 67 item: 'language',
michael@0 68 name: 'commands',
michael@0 69 prompt: ':',
michael@0 70 proportionalFonts: true,
michael@0 71
michael@0 72 constructor: function(terminal) {
michael@0 73 this.terminal = terminal;
michael@0 74 this.document = terminal.document;
michael@0 75 this.focusManager = this.terminal.focusManager;
michael@0 76
michael@0 77 var options = this.terminal.options;
michael@0 78 this.requisition = options.requisition;
michael@0 79 if (this.requisition == null) {
michael@0 80 if (options.environment == null) {
michael@0 81 options.environment = {};
michael@0 82 options.environment.document = options.document || this.document;
michael@0 83 options.environment.window = options.environment.document.defaultView;
michael@0 84 }
michael@0 85
michael@0 86 this.requisition = new Requisition(options);
michael@0 87 }
michael@0 88
michael@0 89 // We also keep track of the last known arg text for the current assignment
michael@0 90 this.lastText = undefined;
michael@0 91
michael@0 92 // Used to effect caret changes. See _processCaretChange()
michael@0 93 this._caretChange = null;
michael@0 94
michael@0 95 // We keep track of which assignment the cursor is in
michael@0 96 this.assignment = this.requisition.getAssignmentAt(0);
michael@0 97
michael@0 98 if (commandHtmlPromise == null) {
michael@0 99 commandHtmlPromise = host.staticRequire(module, './command.html');
michael@0 100 }
michael@0 101
michael@0 102 return commandHtmlPromise.then(function(commandHtml) {
michael@0 103 this.commandDom = util.toDom(this.document, commandHtml);
michael@0 104
michael@0 105 this.requisition.commandOutputManager.onOutput.add(this.outputted, this);
michael@0 106 var mapping = cli.getMapping(this.requisition.executionContext);
michael@0 107 mapping.terminal = this.terminal;
michael@0 108
michael@0 109 return this;
michael@0 110 }.bind(this));
michael@0 111 },
michael@0 112
michael@0 113 destroy: function() {
michael@0 114 var mapping = cli.getMapping(this.requisition.executionContext);
michael@0 115 delete mapping.terminal;
michael@0 116
michael@0 117 this.requisition.commandOutputManager.onOutput.remove(this.outputted, this);
michael@0 118
michael@0 119 this.terminal = undefined;
michael@0 120 this.requisition = undefined;
michael@0 121 this.commandDom = undefined;
michael@0 122 },
michael@0 123
michael@0 124 // From the requisition.textChanged event
michael@0 125 textChanged: function() {
michael@0 126 if (this.terminal == null) {
michael@0 127 return; // This can happen post-destroy()
michael@0 128 }
michael@0 129
michael@0 130 if (this.terminal._caretChange == null) {
michael@0 131 // We weren't expecting a change so this was requested by the hint system
michael@0 132 // we should move the cursor to the end of the 'changed section', and the
michael@0 133 // best we can do for that right now is the end of the current argument.
michael@0 134 this.terminal._caretChange = Caret.TO_ARG_END;
michael@0 135 }
michael@0 136
michael@0 137 var newStr = this.requisition.toString();
michael@0 138 var input = this.terminal.getInputState();
michael@0 139
michael@0 140 input.typed = newStr;
michael@0 141 this._processCaretChange(input);
michael@0 142
michael@0 143 // We don't update terminal._previousValue. Should we?
michael@0 144 // Shouldn't this really be a function of terminal?
michael@0 145 if (this.terminal.inputElement.value !== newStr) {
michael@0 146 this.terminal.inputElement.value = newStr;
michael@0 147 }
michael@0 148 this.terminal.onInputChange({ inputState: input });
michael@0 149
michael@0 150 // We get here for minor things like whitespace change in arg prefix,
michael@0 151 // so we ignore anything but an actual value change.
michael@0 152 if (this.assignment.arg.text === this.lastText) {
michael@0 153 return;
michael@0 154 }
michael@0 155
michael@0 156 this.lastText = this.assignment.arg.text;
michael@0 157
michael@0 158 this.terminal.field.update();
michael@0 159 this.terminal.field.setConversion(this.assignment.conversion);
michael@0 160 util.setTextContent(this.terminal.descriptionEle, this.description);
michael@0 161 },
michael@0 162
michael@0 163 // Called internally whenever we think that the current assignment might
michael@0 164 // have changed, typically on mouse-clicks or key presses.
michael@0 165 caretMoved: function(start) {
michael@0 166 var newAssignment = this.requisition.getAssignmentAt(start);
michael@0 167 if (this.assignment !== newAssignment) {
michael@0 168 if (this.assignment.param.type.onLeave) {
michael@0 169 this.assignment.param.type.onLeave(this.assignment);
michael@0 170 }
michael@0 171
michael@0 172 // This can be kicked off either by requisition doing an assign or by
michael@0 173 // terminal noticing a cursor movement out of a command, so we should
michael@0 174 // check that this really is a new assignment
michael@0 175 var isNew = (this.assignment !== newAssignment);
michael@0 176
michael@0 177 this.assignment = newAssignment;
michael@0 178 this.terminal.updateCompletion();
michael@0 179
michael@0 180 if (isNew) {
michael@0 181 this.updateHints();
michael@0 182 }
michael@0 183
michael@0 184 if (this.assignment.param.type.onEnter) {
michael@0 185 this.assignment.param.type.onEnter(this.assignment);
michael@0 186 }
michael@0 187 }
michael@0 188 else {
michael@0 189 if (this.assignment && this.assignment.param.type.onChange) {
michael@0 190 this.assignment.param.type.onChange(this.assignment);
michael@0 191 }
michael@0 192 }
michael@0 193
michael@0 194 // Warning: compare the logic here with the logic in fieldChanged, which
michael@0 195 // is slightly different. They should probably be the same
michael@0 196 var error = (this.assignment.status === Status.ERROR);
michael@0 197 this.focusManager.setError(error);
michael@0 198 },
michael@0 199
michael@0 200 // Called whenever the assignment that we're providing help with changes
michael@0 201 updateHints: function() {
michael@0 202 this.lastText = this.assignment.arg.text;
michael@0 203
michael@0 204 var field = this.terminal.field;
michael@0 205 if (field) {
michael@0 206 field.onFieldChange.remove(this.terminal.fieldChanged, this.terminal);
michael@0 207 field.destroy();
michael@0 208 }
michael@0 209
michael@0 210 field = this.terminal.field = fields.getField(this.assignment.param.type, {
michael@0 211 document: this.terminal.document,
michael@0 212 requisition: this.requisition
michael@0 213 });
michael@0 214
michael@0 215 this.focusManager.setImportantFieldFlag(field.isImportant);
michael@0 216
michael@0 217 field.onFieldChange.add(this.terminal.fieldChanged, this.terminal);
michael@0 218 field.setConversion(this.assignment.conversion);
michael@0 219
michael@0 220 // Filled in by the template process
michael@0 221 this.terminal.errorEle = undefined;
michael@0 222 this.terminal.descriptionEle = undefined;
michael@0 223
michael@0 224 var contents = this.terminal.tooltipTemplate.cloneNode(true);
michael@0 225 domtemplate.template(contents, this.terminal, {
michael@0 226 blankNullUndefined: true,
michael@0 227 stack: 'terminal.html#tooltip'
michael@0 228 });
michael@0 229
michael@0 230 util.clearElement(this.terminal.tooltipElement);
michael@0 231 this.terminal.tooltipElement.appendChild(contents);
michael@0 232 this.terminal.tooltipElement.style.display = 'block';
michael@0 233
michael@0 234 field.setMessageElement(this.terminal.errorEle);
michael@0 235 },
michael@0 236
michael@0 237 /**
michael@0 238 * See also handleDownArrow for some symmetry
michael@0 239 */
michael@0 240 handleUpArrow: function() {
michael@0 241 // If the user is on a valid value, then we increment the value, but if
michael@0 242 // they've typed something that's not right we page through predictions
michael@0 243 if (this.assignment.getStatus() === Status.VALID) {
michael@0 244 return this.requisition.increment(this.assignment).then(function() {
michael@0 245 this.textChanged();
michael@0 246 this.focusManager.onInputChange();
michael@0 247 return true;
michael@0 248 }.bind(this));
michael@0 249 }
michael@0 250
michael@0 251 return promise.resolve(false);
michael@0 252 },
michael@0 253
michael@0 254 /**
michael@0 255 * See also handleUpArrow for some symmetry
michael@0 256 */
michael@0 257 handleDownArrow: function() {
michael@0 258 if (this.assignment.getStatus() === Status.VALID) {
michael@0 259 return this.requisition.decrement(this.assignment).then(function() {
michael@0 260 this.textChanged();
michael@0 261 this.focusManager.onInputChange();
michael@0 262 return true;
michael@0 263 }.bind(this));
michael@0 264 }
michael@0 265
michael@0 266 return promise.resolve(false);
michael@0 267 },
michael@0 268
michael@0 269 /**
michael@0 270 * RETURN checks status and might exec
michael@0 271 */
michael@0 272 handleReturn: function(input) {
michael@0 273 // Deny RETURN unless the command might work
michael@0 274 if (this.requisition.status !== Status.VALID) {
michael@0 275 return promise.resolve(false);
michael@0 276 }
michael@0 277
michael@0 278 this.terminal.history.add(input);
michael@0 279 this.terminal.unsetChoice();
michael@0 280
michael@0 281 return this.requisition.exec().then(function() {
michael@0 282 this.textChanged();
michael@0 283 return true;
michael@0 284 }.bind(this));
michael@0 285 },
michael@0 286
michael@0 287 /**
michael@0 288 * Warning: We get TAB events for more than just the user pressing TAB in our
michael@0 289 * input element.
michael@0 290 */
michael@0 291 handleTab: function() {
michael@0 292 // It's possible for TAB to not change the input, in which case the
michael@0 293 // textChanged event will not fire, and the caret move will not be
michael@0 294 // processed. So we check that this is done first
michael@0 295 this.terminal._caretChange = Caret.TO_ARG_END;
michael@0 296 var inputState = this.terminal.getInputState();
michael@0 297 this._processCaretChange(inputState);
michael@0 298
michael@0 299 this.terminal._previousValue = this.terminal.inputElement.value;
michael@0 300
michael@0 301 // The changes made by complete may happen asynchronously, so after the
michael@0 302 // the call to complete() we should avoid making changes before the end
michael@0 303 // of the event loop
michael@0 304 var index = this.terminal.getChoiceIndex();
michael@0 305 return this.requisition.complete(inputState.cursor, index).then(function(updated) {
michael@0 306 // Abort UI changes if this UI update has been overtaken
michael@0 307 if (!updated) {
michael@0 308 return RESOLVED;
michael@0 309 }
michael@0 310 this.textChanged();
michael@0 311 return this.terminal.unsetChoice();
michael@0 312 }.bind(this));
michael@0 313 },
michael@0 314
michael@0 315 /**
michael@0 316 * The input text has changed in some way.
michael@0 317 */
michael@0 318 handleInput: function(value) {
michael@0 319 this.terminal._caretChange = Caret.NO_CHANGE;
michael@0 320
michael@0 321 return this.requisition.update(value).then(function(updated) {
michael@0 322 // Abort UI changes if this UI update has been overtaken
michael@0 323 if (!updated) {
michael@0 324 return RESOLVED;
michael@0 325 }
michael@0 326 this.textChanged();
michael@0 327 return this.terminal.unsetChoice();
michael@0 328 }.bind(this));
michael@0 329 },
michael@0 330
michael@0 331 /**
michael@0 332 * Counterpart to |setInput| for moving the cursor.
michael@0 333 * @param cursor A JS object shaped like { start: x, end: y }
michael@0 334 */
michael@0 335 setCursor: function(cursor) {
michael@0 336 this._caretChange = Caret.NO_CHANGE;
michael@0 337 this._processCaretChange({
michael@0 338 typed: this.terminal.inputElement.value,
michael@0 339 cursor: cursor
michael@0 340 });
michael@0 341 },
michael@0 342
michael@0 343 /**
michael@0 344 * If this._caretChange === Caret.TO_ARG_END, we alter the input object to move
michael@0 345 * the selection start to the end of the current argument.
michael@0 346 * @param input An object shaped like { typed:'', cursor: { start:0, end:0 }}
michael@0 347 */
michael@0 348 _processCaretChange: function(input) {
michael@0 349 var start, end;
michael@0 350 switch (this._caretChange) {
michael@0 351 case Caret.SELECT_ALL:
michael@0 352 start = 0;
michael@0 353 end = input.typed.length;
michael@0 354 break;
michael@0 355
michael@0 356 case Caret.TO_END:
michael@0 357 start = input.typed.length;
michael@0 358 end = input.typed.length;
michael@0 359 break;
michael@0 360
michael@0 361 case Caret.TO_ARG_END:
michael@0 362 // There could be a fancy way to do this involving assignment/arg math
michael@0 363 // but it doesn't seem easy, so we cheat a move the cursor to just before
michael@0 364 // the next space, or the end of the input
michael@0 365 start = input.cursor.start;
michael@0 366 do {
michael@0 367 start++;
michael@0 368 }
michael@0 369 while (start < input.typed.length && input.typed[start - 1] !== ' ');
michael@0 370
michael@0 371 end = start;
michael@0 372 break;
michael@0 373
michael@0 374 default:
michael@0 375 start = input.cursor.start;
michael@0 376 end = input.cursor.end;
michael@0 377 break;
michael@0 378 }
michael@0 379
michael@0 380 start = (start > input.typed.length) ? input.typed.length : start;
michael@0 381 end = (end > input.typed.length) ? input.typed.length : end;
michael@0 382
michael@0 383 var newInput = {
michael@0 384 typed: input.typed,
michael@0 385 cursor: { start: start, end: end }
michael@0 386 };
michael@0 387
michael@0 388 if (this.terminal.inputElement.selectionStart !== start) {
michael@0 389 this.terminal.inputElement.selectionStart = start;
michael@0 390 }
michael@0 391 if (this.terminal.inputElement.selectionEnd !== end) {
michael@0 392 this.terminal.inputElement.selectionEnd = end;
michael@0 393 }
michael@0 394
michael@0 395 this.caretMoved(start);
michael@0 396
michael@0 397 this._caretChange = null;
michael@0 398 return newInput;
michael@0 399 },
michael@0 400
michael@0 401 /**
michael@0 402 * Calculate the properties required by the template process for completer.html
michael@0 403 */
michael@0 404 getCompleterTemplateData: function() {
michael@0 405 var input = this.terminal.getInputState();
michael@0 406 var start = input.cursor.start;
michael@0 407 var index = this.terminal.getChoiceIndex();
michael@0 408
michael@0 409 return this.requisition.getStateData(start, index).then(function(data) {
michael@0 410 // Calculate the statusMarkup required to show wavy lines underneath the
michael@0 411 // input text (like that of an inline spell-checker) which used by the
michael@0 412 // template process for completer.html
michael@0 413 // i.e. s/space/&nbsp/g in the string (for HTML display) and status to an
michael@0 414 // appropriate class name (i.e. lower cased, prefixed with gcli-in-)
michael@0 415 data.statusMarkup.forEach(function(member) {
michael@0 416 member.string = member.string.replace(/ /g, '\u00a0'); // i.e. &nbsp;
michael@0 417 member.className = 'gcli-in-' + member.status.toString().toLowerCase();
michael@0 418 }, this);
michael@0 419
michael@0 420 return data;
michael@0 421 });
michael@0 422 },
michael@0 423
michael@0 424 /**
michael@0 425 * Called by the onFieldChange event (via the terminal) on the current Field
michael@0 426 */
michael@0 427 fieldChanged: function(ev) {
michael@0 428 this.requisition.setAssignment(this.assignment, ev.conversion.arg,
michael@0 429 { matchPadding: true }).then(function() {
michael@0 430 this.textChanged();
michael@0 431 }.bind(this));
michael@0 432
michael@0 433 var isError = ev.conversion.message != null && ev.conversion.message !== '';
michael@0 434 this.focusManager.setError(isError);
michael@0 435 },
michael@0 436
michael@0 437 /**
michael@0 438 * Monitor for new command executions
michael@0 439 */
michael@0 440 outputted: function(ev) {
michael@0 441 if (ev.output.hidden) {
michael@0 442 return;
michael@0 443 }
michael@0 444
michael@0 445 var template = this.commandDom.cloneNode(true);
michael@0 446 var templateOptions = { stack: 'terminal.html#outputView' };
michael@0 447
michael@0 448 var context = this.requisition.conversionContext;
michael@0 449 var data = {
michael@0 450 onclick: context.update,
michael@0 451 ondblclick: context.updateExec,
michael@0 452 language: this,
michael@0 453 output: ev.output,
michael@0 454 promptClass: (ev.output.error ? 'gcli-row-error' : '') +
michael@0 455 (ev.output.completed ? ' gcli-row-complete' : ''),
michael@0 456 // Elements attached to this by template().
michael@0 457 rowinEle: null,
michael@0 458 rowoutEle: null,
michael@0 459 throbEle: null,
michael@0 460 promptEle: null
michael@0 461 };
michael@0 462
michael@0 463 domtemplate.template(template, data, templateOptions);
michael@0 464
michael@0 465 ev.output.promise.then(function() {
michael@0 466 var document = data.rowoutEle.ownerDocument;
michael@0 467
michael@0 468 if (ev.output.completed) {
michael@0 469 data.promptEle.classList.add('gcli-row-complete');
michael@0 470 }
michael@0 471 if (ev.output.error) {
michael@0 472 data.promptEle.classList.add('gcli-row-error');
michael@0 473 }
michael@0 474
michael@0 475 util.clearElement(data.rowoutEle);
michael@0 476
michael@0 477 return ev.output.convert('dom', context).then(function(node) {
michael@0 478 this._linksToNewTab(node);
michael@0 479 data.rowoutEle.appendChild(node);
michael@0 480
michael@0 481 var event = document.createEvent('Event');
michael@0 482 event.initEvent('load', true, true);
michael@0 483 event.addedElement = node;
michael@0 484 node.dispatchEvent(event);
michael@0 485
michael@0 486 this.terminal.scrollToBottom();
michael@0 487 data.throbEle.style.display = ev.output.completed ? 'none' : 'block';
michael@0 488 }.bind(this));
michael@0 489 }.bind(this)).then(null, console.error);
michael@0 490
michael@0 491 this.terminal.addElement(data.rowinEle);
michael@0 492 this.terminal.addElement(data.rowoutEle);
michael@0 493 this.terminal.scrollToBottom();
michael@0 494
michael@0 495 this.focusManager.outputted();
michael@0 496 },
michael@0 497
michael@0 498 /**
michael@0 499 * Find elements with href attributes and add a target=_blank so opened links
michael@0 500 * will open in a new window
michael@0 501 */
michael@0 502 _linksToNewTab: function(element) {
michael@0 503 var links = element.querySelectorAll('*[href]');
michael@0 504 for (var i = 0; i < links.length; i++) {
michael@0 505 links[i].setAttribute('target', '_blank');
michael@0 506 }
michael@0 507 return element;
michael@0 508 },
michael@0 509
michael@0 510 /**
michael@0 511 * Show a short introduction to this language
michael@0 512 */
michael@0 513 showIntro: function() {
michael@0 514 intro.maybeShowIntro(this.requisition.commandOutputManager,
michael@0 515 this.requisition.conversionContext);
michael@0 516 },
michael@0 517 };
michael@0 518
michael@0 519 /**
michael@0 520 * The description (displayed at the top of the hint area) should be blank if
michael@0 521 * we're entering the CommandAssignment (because it's obvious) otherwise it's
michael@0 522 * the parameter description.
michael@0 523 */
michael@0 524 Object.defineProperty(commandLanguage, 'description', {
michael@0 525 get: function() {
michael@0 526 if (this.assignment == null || (
michael@0 527 this.assignment instanceof CommandAssignment &&
michael@0 528 this.assignment.value == null)) {
michael@0 529 return '';
michael@0 530 }
michael@0 531
michael@0 532 return this.assignment.param.manual || this.assignment.param.description;
michael@0 533 },
michael@0 534 enumerable: true
michael@0 535 });
michael@0 536
michael@0 537 /**
michael@0 538 * Present an error message to the hint popup
michael@0 539 */
michael@0 540 Object.defineProperty(commandLanguage, 'message', {
michael@0 541 get: function() {
michael@0 542 return this.assignment.conversion.message;
michael@0 543 },
michael@0 544 enumerable: true
michael@0 545 });
michael@0 546
michael@0 547 exports.items = [ commandLanguage ];

mercurial