1/* 2 * Copyright (C) 2012 Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31importScript("cm/codemirror.js"); 32importScript("cm/css.js"); 33importScript("cm/javascript.js"); 34importScript("cm/xml.js"); 35importScript("cm/htmlmixed.js"); 36importScript("cm/matchbrackets.js"); 37importScript("cm/closebrackets.js"); 38 39/** 40 * @constructor 41 * @extends {WebInspector.View} 42 * @implements {WebInspector.TextEditor} 43 * @param {?string} url 44 * @param {WebInspector.TextEditorDelegate} delegate 45 */ 46WebInspector.CodeMirrorTextEditor = function(url, delegate) 47{ 48 WebInspector.View.call(this); 49 this._delegate = delegate; 50 this._url = url; 51 52 this.registerRequiredCSS("cm/codemirror.css"); 53 this.registerRequiredCSS("cm/cmdevtools.css"); 54 55 this._codeMirror = window.CodeMirror(this.element, { 56 lineNumbers: true, 57 gutters: ["CodeMirror-linenumbers", "breakpoints"], 58 matchBrackets: true, 59 autoCloseBrackets: WebInspector.experimentsSettings.textEditorSmartBraces.isEnabled() 60 }); 61 62 var indent = WebInspector.settings.textEditorIndent.get(); 63 if (indent === WebInspector.TextUtils.Indent.TabCharacter) { 64 this._codeMirror.setOption("indentWithTabs", true); 65 this._codeMirror.setOption("indentUnit", 4); 66 } else { 67 this._codeMirror.setOption("indentWithTabs", false); 68 this._codeMirror.setOption("indentUnit", indent.length); 69 } 70 71 this._tokenHighlighter = new WebInspector.CodeMirrorTextEditor.TokenHighlighter(this._codeMirror); 72 this._blockIndentController = new WebInspector.CodeMirrorTextEditor.BlockIndentController(this._codeMirror); 73 this._fixWordMovement = new WebInspector.CodeMirrorTextEditor.FixWordMovement(this._codeMirror); 74 75 this._codeMirror.on("change", this._change.bind(this)); 76 this._codeMirror.on("gutterClick", this._gutterClick.bind(this)); 77 this._codeMirror.on("cursorActivity", this._cursorActivity.bind(this)); 78 this._codeMirror.on("scroll", this._scroll.bind(this)); 79 this.element.addEventListener("contextmenu", this._contextMenu.bind(this)); 80 81 this._lastRange = this.range(); 82 83 this.element.firstChild.addStyleClass("source-code"); 84 this.element.addStyleClass("fill"); 85 this.markAsLayoutBoundary(); 86 87 this._elementToWidget = new Map(); 88 this._nestedUpdatesCounter = 0; 89 90 this.element.addEventListener("focus", this._handleElementFocus.bind(this), false); 91 this.element.tabIndex = 0; 92} 93 94WebInspector.CodeMirrorTextEditor.prototype = { 95 96 /** 97 * @param {number} lineNumber 98 * @param {number} column 99 * @return {?{x: number, y: number, height: number}} 100 */ 101 cursorPositionToCoordinates: function(lineNumber, column) 102 { 103 if (lineNumber >= this._codeMirror.lineCount || column > this._codeMirror.getLine(lineNumber).length || lineNumber < 0 || column < 0) 104 return null; 105 106 var metrics = this._codeMirror.cursorCoords(CodeMirror.Pos(lineNumber, column)); 107 108 return { 109 x: metrics.left, 110 y: metrics.top, 111 height: metrics.bottom - metrics.top 112 }; 113 }, 114 115 /** 116 * @param {number} x 117 * @param {number} y 118 * @return {?WebInspector.TextRange} 119 */ 120 coordinatesToCursorPosition: function(x, y) 121 { 122 var element = document.elementFromPoint(x, y); 123 if (!element || !element.isSelfOrDescendant(this._codeMirror.getWrapperElement())) 124 return null; 125 var gutterBox = this._codeMirror.getGutterElement().boxInWindow(); 126 if (x >= gutterBox.x && x <= gutterBox.x + gutterBox.width && 127 y >= gutterBox.y && y <= gutterBox.y + gutterBox.height) 128 return null; 129 var coords = this._codeMirror.coordsChar({left: x, top: y}); 130 ++coords.ch; 131 return this._toRange(coords, coords); 132 }, 133 134 /** 135 * @param {number} lineNumber 136 * @param {number} column 137 * @return {?{startColumn: number, endColumn: number, token: string}} 138 */ 139 tokenAtTextPosition: function(lineNumber, column) 140 { 141 if (lineNumber < 0 || lineNumber >= this._codeMirror.lineCount()) 142 return null; 143 var token = this._codeMirror.getTokenAt(CodeMirror.Pos(lineNumber, column || 1)); 144 if (!token || !token.type) 145 return null; 146 var convertedType = null; 147 if (token.type.startsWith("variable") || token.type.startsWith("property")) { 148 return { 149 startColumn: token.start, 150 endColumn: token.end - 1, 151 type: "javascript-ident" 152 }; 153 } 154 return null; 155 }, 156 157 /** 158 * @param {WebInspector.TextRange} textRange 159 * @return {string} 160 */ 161 copyRange: function(textRange) 162 { 163 var pos = this._toPos(textRange); 164 return this._codeMirror.getRange(pos.start, pos.end); 165 }, 166 167 /** 168 * @return {boolean} 169 */ 170 isClean: function() 171 { 172 return this._codeMirror.isClean(); 173 }, 174 175 markClean: function() 176 { 177 this._codeMirror.markClean(); 178 }, 179 180 /** 181 * @param {string} mimeType 182 */ 183 set mimeType(mimeType) 184 { 185 this._codeMirror.setOption("mode", mimeType); 186 switch(mimeType) { 187 case "text/html": this._codeMirror.setOption("theme", "web-inspector-html"); break; 188 case "text/css": this._codeMirror.setOption("theme", "web-inspector-css"); break; 189 case "text/javascript": this._codeMirror.setOption("theme", "web-inspector-js"); break; 190 } 191 }, 192 193 /** 194 * @param {boolean} readOnly 195 */ 196 setReadOnly: function(readOnly) 197 { 198 this._codeMirror.setOption("readOnly", readOnly ? "nocursor" : false); 199 }, 200 201 /** 202 * @return {boolean} 203 */ 204 readOnly: function() 205 { 206 return !!this._codeMirror.getOption("readOnly"); 207 }, 208 209 /** 210 * @param {Object} highlightDescriptor 211 */ 212 removeHighlight: function(highlightDescriptor) 213 { 214 highlightDescriptor.clear(); 215 }, 216 217 /** 218 * @param {WebInspector.TextRange} range 219 * @param {string} cssClass 220 * @return {Object} 221 */ 222 highlightRange: function(range, cssClass) 223 { 224 var pos = this._toPos(range); 225 ++pos.end.ch; 226 return this._codeMirror.markText(pos.start, pos.end, { 227 className: cssClass, 228 startStyle: cssClass + "-start", 229 endStyle: cssClass + "-end" 230 }); 231 }, 232 233 /** 234 * @return {Element} 235 */ 236 defaultFocusedElement: function() 237 { 238 return this.element; 239 }, 240 241 focus: function() 242 { 243 this._codeMirror.focus(); 244 }, 245 246 _handleElementFocus: function() 247 { 248 this._codeMirror.focus(); 249 }, 250 251 beginUpdates: function() 252 { 253 ++this._nestedUpdatesCounter; 254 }, 255 256 endUpdates: function() 257 { 258 if (!--this._nestedUpdatesCounter); 259 this._codeMirror.refresh(); 260 }, 261 262 /** 263 * @param {number} lineNumber 264 */ 265 revealLine: function(lineNumber) 266 { 267 var pos = CodeMirror.Pos(lineNumber, 0); 268 var topLine = this._topScrolledLine(); 269 var bottomLine = this._bottomScrolledLine(); 270 271 var margin = null; 272 var lineMargin = 3; 273 var scrollInfo = this._codeMirror.getScrollInfo(); 274 if ((lineNumber < topLine + lineMargin) || (lineNumber >= bottomLine - lineMargin)) { 275 // scrollIntoView could get into infinite loop if margin exceeds half of the clientHeight. 276 margin = (scrollInfo.clientHeight*0.9/2) >>> 0; 277 } 278 this._codeMirror.scrollIntoView(pos, margin); 279 }, 280 281 _gutterClick: function(instance, lineNumber, gutter, event) 282 { 283 this.dispatchEventToListeners(WebInspector.TextEditor.Events.GutterClick, { lineNumber: lineNumber, event: event }); 284 }, 285 286 _contextMenu: function(event) 287 { 288 var contextMenu = new WebInspector.ContextMenu(event); 289 var target = event.target.enclosingNodeOrSelfWithClass("CodeMirror-gutter-elt"); 290 if (target) 291 this._delegate.populateLineGutterContextMenu(contextMenu, parseInt(target.textContent, 10) - 1); 292 else 293 this._delegate.populateTextAreaContextMenu(contextMenu, null); 294 contextMenu.show(); 295 }, 296 297 /** 298 * @param {number} lineNumber 299 * @param {boolean} disabled 300 * @param {boolean} conditional 301 */ 302 addBreakpoint: function(lineNumber, disabled, conditional) 303 { 304 var element = document.createElement("span"); 305 element.textContent = lineNumber + 1; 306 element.className = "cm-breakpoint" + (disabled ? " cm-breakpoint-disabled" : "") + (conditional ? " cm-breakpoint-conditional" : ""); 307 this._codeMirror.setGutterMarker(lineNumber, "breakpoints", element); 308 }, 309 310 /** 311 * @param {number} lineNumber 312 */ 313 removeBreakpoint: function(lineNumber) 314 { 315 this._codeMirror.setGutterMarker(lineNumber, "breakpoints", null); 316 }, 317 318 /** 319 * @param {number} lineNumber 320 */ 321 setExecutionLine: function(lineNumber) 322 { 323 this._executionLine = this._codeMirror.getLineHandle(lineNumber); 324 this._codeMirror.addLineClass(this._executionLine, null, "cm-execution-line"); 325 }, 326 327 clearExecutionLine: function() 328 { 329 if (this._executionLine) 330 this._codeMirror.removeLineClass(this._executionLine, null, "cm-execution-line"); 331 delete this._executionLine; 332 }, 333 334 /** 335 * @param {number} lineNumber 336 * @param {Element} element 337 */ 338 addDecoration: function(lineNumber, element) 339 { 340 var widget = this._codeMirror.addLineWidget(lineNumber, element); 341 this._elementToWidget.put(element, widget); 342 }, 343 344 /** 345 * @param {number} lineNumber 346 * @param {Element} element 347 */ 348 removeDecoration: function(lineNumber, element) 349 { 350 var widget = this._elementToWidget.remove(element); 351 if (widget) 352 this._codeMirror.removeLineWidget(widget); 353 }, 354 355 /** 356 * @param {WebInspector.TextRange} range 357 */ 358 markAndRevealRange: function(range) 359 { 360 if (range) 361 this.setSelection(range); 362 }, 363 364 /** 365 * @param {number} lineNumber 366 */ 367 highlightLine: function(lineNumber) 368 { 369 this.clearLineHighlight(); 370 this._highlightedLine = this._codeMirror.getLineHandle(lineNumber); 371 if (!this._highlightedLine) 372 return; 373 this.revealLine(lineNumber); 374 this._codeMirror.addLineClass(this._highlightedLine, null, "cm-highlight"); 375 this._clearHighlightTimeout = setTimeout(this.clearLineHighlight.bind(this), 2000); 376 }, 377 378 clearLineHighlight: function() 379 { 380 if (this._clearHighlightTimeout) 381 clearTimeout(this._clearHighlightTimeout); 382 delete this._clearHighlightTimeout; 383 384 if (this._highlightedLine) 385 this._codeMirror.removeLineClass(this._highlightedLine, null, "cm-highlight"); 386 delete this._highlightedLine; 387 }, 388 389 /** 390 * @return {Array.<Element>} 391 */ 392 elementsToRestoreScrollPositionsFor: function() 393 { 394 return []; 395 }, 396 397 /** 398 * @param {WebInspector.TextEditor} textEditor 399 */ 400 inheritScrollPositions: function(textEditor) 401 { 402 }, 403 404 onResize: function() 405 { 406 this._codeMirror.refresh(); 407 }, 408 409 /** 410 * @param {WebInspector.TextRange} range 411 * @param {string} text 412 * @return {WebInspector.TextRange} 413 */ 414 editRange: function(range, text) 415 { 416 var pos = this._toPos(range); 417 this._codeMirror.replaceRange(text, pos.start, pos.end); 418 var newRange = this._toRange(pos.start, this._codeMirror.posFromIndex(this._codeMirror.indexFromPos(pos.start) + text.length)); 419 this._delegate.onTextChanged(range, newRange); 420 return newRange; 421 }, 422 423 _change: function() 424 { 425 var widgets = this._elementToWidget.values(); 426 for (var i = 0; i < widgets.length; ++i) 427 this._codeMirror.removeLineWidget(widgets[i]); 428 this._elementToWidget.clear(); 429 430 var newRange = this.range(); 431 this._delegate.onTextChanged(this._lastRange, newRange); 432 this._lastRange = newRange; 433 }, 434 435 _cursorActivity: function() 436 { 437 var start = this._codeMirror.getCursor("anchor"); 438 var end = this._codeMirror.getCursor("head"); 439 this._delegate.selectionChanged(this._toRange(start, end)); 440 }, 441 442 _coordsCharLocal: function(coords) 443 { 444 var top = coords.top; 445 var totalLines = this._codeMirror.lineCount(); 446 var begin = 0; 447 var end = totalLines - 1; 448 while (end - begin > 1) { 449 var middle = (begin + end) >> 1; 450 var coords = this._codeMirror.charCoords(CodeMirror.Pos(middle, 0), "local"); 451 if (coords.top >= top) 452 end = middle; 453 else 454 begin = middle; 455 } 456 457 return end; 458 }, 459 460 _topScrolledLine: function() 461 { 462 var scrollInfo = this._codeMirror.getScrollInfo(); 463 // Workaround for CodeMirror's coordsChar incorrect result for "local" mode. 464 return this._coordsCharLocal(scrollInfo); 465 }, 466 467 _bottomScrolledLine: function() 468 { 469 var scrollInfo = this._codeMirror.getScrollInfo(); 470 scrollInfo.top += scrollInfo.clientHeight; 471 // Workaround for CodeMirror's coordsChar incorrect result for "local" mode. 472 return this._coordsCharLocal(scrollInfo); 473 }, 474 475 _scroll: function() 476 { 477 this._delegate.scrollChanged(this._topScrolledLine()); 478 }, 479 480 /** 481 * @param {number} lineNumber 482 */ 483 scrollToLine: function(lineNumber) 484 { 485 function performScroll() 486 { 487 var pos = CodeMirror.Pos(lineNumber, 0); 488 var coords = this._codeMirror.charCoords(pos, "local"); 489 this._codeMirror.scrollTo(0, coords.top); 490 } 491 492 setTimeout(performScroll.bind(this), 0); 493 }, 494 495 /** 496 * @return {WebInspector.TextRange} 497 */ 498 selection: function(textRange) 499 { 500 var start = this._codeMirror.getCursor(true); 501 var end = this._codeMirror.getCursor(false); 502 503 if (start.line > end.line || (start.line == end.line && start.ch > end.ch)) 504 return this._toRange(end, start); 505 506 return this._toRange(start, end); 507 }, 508 509 /** 510 * @return {WebInspector.TextRange?} 511 */ 512 lastSelection: function() 513 { 514 return this._lastSelection; 515 }, 516 517 /** 518 * @param {WebInspector.TextRange} textRange 519 */ 520 setSelection: function(textRange) 521 { 522 function performSelectionSet() 523 { 524 this._lastSelection = textRange; 525 var pos = this._toPos(textRange); 526 this._codeMirror.setSelection(pos.start, pos.end); 527 } 528 529 setTimeout(performSelectionSet.bind(this), 0); 530 }, 531 532 /** 533 * @param {string} text 534 */ 535 setText: function(text) 536 { 537 this._codeMirror.setValue(text); 538 }, 539 540 /** 541 * @return {string} 542 */ 543 text: function() 544 { 545 return this._codeMirror.getValue(); 546 }, 547 548 /** 549 * @return {WebInspector.TextRange} 550 */ 551 range: function() 552 { 553 var lineCount = this.linesCount; 554 var lastLine = this._codeMirror.getLine(lineCount - 1); 555 return this._toRange({ line: 0, ch: 0 }, { line: lineCount - 1, ch: lastLine.length }); 556 }, 557 558 /** 559 * @param {number} lineNumber 560 * @return {string} 561 */ 562 line: function(lineNumber) 563 { 564 return this._codeMirror.getLine(lineNumber); 565 }, 566 567 /** 568 * @return {number} 569 */ 570 get linesCount() 571 { 572 return this._codeMirror.lineCount(); 573 }, 574 575 /** 576 * @param {number} line 577 * @param {string} name 578 * @param {Object?} value 579 */ 580 setAttribute: function(line, name, value) 581 { 582 var handle = this._codeMirror.getLineHandle(line); 583 if (handle.attributes === undefined) handle.attributes = {}; 584 handle.attributes[name] = value; 585 }, 586 587 /** 588 * @param {number} line 589 * @param {string} name 590 * @return {Object|null} value 591 */ 592 getAttribute: function(line, name) 593 { 594 var handle = this._codeMirror.getLineHandle(line); 595 return handle.attributes && handle.attributes[name] !== undefined ? handle.attributes[name] : null; 596 }, 597 598 /** 599 * @param {number} line 600 * @param {string} name 601 */ 602 removeAttribute: function(line, name) 603 { 604 var handle = this._codeMirror.getLineHandle(line); 605 if (handle && handle.attributes) 606 delete handle.attributes[name]; 607 }, 608 609 _toPos: function(range) 610 { 611 return { 612 start: {line: range.startLine, ch: range.startColumn}, 613 end: {line: range.endLine, ch: range.endColumn} 614 } 615 }, 616 617 _toRange: function(start, end) 618 { 619 return new WebInspector.TextRange(start.line, start.ch, end.line, end.ch); 620 }, 621 622 __proto__: WebInspector.View.prototype 623} 624 625WebInspector.CodeMirrorTextEditor.TokenHighlighter = function(codeMirror) 626{ 627 this._codeMirror = codeMirror; 628 this._codeMirror.on("cursorActivity", this._cursorChange.bind(this)); 629} 630 631WebInspector.CodeMirrorTextEditor.TokenHighlighter.prototype = { 632 _cursorChange: function() 633 { 634 this._codeMirror.operation(this._removeHighlight.bind(this)); 635 var selectionStart = this._codeMirror.getCursor("start"); 636 var selectionEnd = this._codeMirror.getCursor("end"); 637 if (selectionStart.line !== selectionEnd.line) 638 return; 639 if (selectionStart.ch === selectionEnd.ch) 640 return; 641 642 var selectedText = this._codeMirror.getSelection(); 643 if (this._isWord(selectedText, selectionStart.line, selectionStart.ch, selectionEnd.ch)) 644 this._codeMirror.operation(this._addHighlight.bind(this, selectedText, selectionStart)); 645 }, 646 647 _isWord: function(selectedText, lineNumber, startColumn, endColumn) 648 { 649 var line = this._codeMirror.getLine(lineNumber); 650 var leftBound = startColumn === 0 || !WebInspector.TextUtils.isWordChar(line.charAt(startColumn - 1)); 651 var rightBound = endColumn === line.length || !WebInspector.TextUtils.isWordChar(line.charAt(endColumn)); 652 return leftBound && rightBound && WebInspector.TextUtils.isWord(selectedText); 653 }, 654 655 _removeHighlight: function() 656 { 657 if (this._highlightDescriptor) { 658 this._codeMirror.removeOverlay(this._highlightDescriptor.overlay); 659 this._codeMirror.removeLineClass(this._highlightDescriptor.selectionStart.line, "wrap", "cm-line-with-selection"); 660 delete this._highlightDescriptor; 661 } 662 }, 663 664 _addHighlight: function(token, selectionStart) 665 { 666 const tokenFirstChar = token.charAt(0); 667 function nextToken(stream) 668 { 669 if (stream.match(token) && (stream.eol() || !WebInspector.TextUtils.isWordChar(stream.peek()))) 670 return stream.column() === selectionStart.ch ? "token-highlight column-with-selection" : "token-highlight"; 671 672 var eatenChar; 673 do { 674 eatenChar = stream.next(); 675 } while (eatenChar && (WebInspector.TextUtils.isWordChar(eatenChar) || stream.peek() !== tokenFirstChar)); 676 } 677 678 var overlayMode = { 679 token: nextToken 680 }; 681 this._codeMirror.addOverlay(overlayMode); 682 this._codeMirror.addLineClass(selectionStart.line, "wrap", "cm-line-with-selection") 683 this._highlightDescriptor = { 684 overlay: overlayMode, 685 selectionStart: selectionStart 686 }; 687 } 688} 689 690WebInspector.CodeMirrorTextEditor.BlockIndentController = function(codeMirror) 691{ 692 codeMirror.addKeyMap(this); 693} 694 695WebInspector.CodeMirrorTextEditor.BlockIndentController.prototype = { 696 name: "blockIndentKeymap", 697 698 Enter: function(codeMirror) 699 { 700 if (codeMirror.somethingSelected()) 701 return CodeMirror.Pass; 702 var cursor = codeMirror.getCursor(); 703 var line = codeMirror.getLine(cursor.line); 704 if (line.substr(cursor.ch - 1, 2) === "{}") { 705 codeMirror.execCommand("newlineAndIndent"); 706 codeMirror.setCursor(cursor); 707 codeMirror.execCommand("newlineAndIndent"); 708 } else 709 return CodeMirror.Pass; 710 } 711} 712 713WebInspector.CodeMirrorTextEditor.FixWordMovement = function(codeMirror) 714{ 715 function moveLeft(shift, codeMirror) 716 { 717 var cursor = codeMirror.getCursor("head"); 718 if (cursor.ch !== 0 || cursor.line === 0) 719 return CodeMirror.Pass; 720 codeMirror.setExtending(shift); 721 codeMirror.execCommand("goLineUp"); 722 codeMirror.execCommand("goLineEnd") 723 codeMirror.setExtending(false); 724 } 725 function moveRight(shift, codeMirror) 726 { 727 var cursor = codeMirror.getCursor("head"); 728 var line = codeMirror.getLine(cursor.line); 729 if (cursor.ch !== line.length || cursor.line + 1 === codeMirror.lineCount()) 730 return CodeMirror.Pass; 731 codeMirror.setExtending(shift); 732 codeMirror.execCommand("goLineDown"); 733 codeMirror.execCommand("goLineStart"); 734 codeMirror.setExtending(false); 735 } 736 737 var modifierKey = WebInspector.isMac() ? "Alt" : "Ctrl"; 738 var leftKey = modifierKey + "-Left"; 739 var rightKey = modifierKey + "-Right"; 740 var keyMap = {}; 741 keyMap[leftKey] = moveLeft.bind(this, false); 742 keyMap[rightKey] = moveRight.bind(this, false); 743 keyMap["Shift-" + leftKey] = moveLeft.bind(this, true); 744 keyMap["Shift-" + rightKey] = moveRight.bind(this, true); 745 codeMirror.addKeyMap(keyMap); 746} 747