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 31 importScript("cm/codemirror.js"); 32 importScript("cm/css.js"); 33 importScript("cm/javascript.js"); 34 importScript("cm/xml.js"); 35 importScript("cm/htmlmixed.js"); 36 37 importScript("cm/matchbrackets.js"); 38 importScript("cm/closebrackets.js"); 39 importScript("cm/markselection.js"); 40 importScript("cm/comment.js"); 41 importScript("cm/overlay.js"); 42 43 importScript("cm/htmlembedded.js"); 44 importScript("cm/clike.js"); 45 importScript("cm/coffeescript.js"); 46 importScript("cm/php.js"); 47 importScript("cm/python.js"); 48 importScript("cm/shell.js"); 49 50 /** 51 * @constructor 52 * @extends {WebInspector.View} 53 * @implements {WebInspector.TextEditor} 54 * @param {?string} url 55 * @param {WebInspector.TextEditorDelegate} delegate 56 */ 57 WebInspector.CodeMirrorTextEditor = function(url, delegate) 58 { 59 WebInspector.View.call(this); 60 this._delegate = delegate; 61 this._url = url; 62 63 this.registerRequiredCSS("cm/codemirror.css"); 64 this.registerRequiredCSS("cm/cmdevtools.css"); 65 66 this._codeMirror = window.CodeMirror(this.element, { 67 lineNumbers: true, 68 gutters: ["CodeMirror-linenumbers"], 69 matchBrackets: true, 70 smartIndent: false, 71 styleSelectedText: true, 72 electricChars: false, 73 autoCloseBrackets: { explode: false } 74 }); 75 this._codeMirror._codeMirrorTextEditor = this; 76 77 CodeMirror.keyMap["devtools-common"] = { 78 "Left": "goCharLeft", 79 "Right": "goCharRight", 80 "Up": "goLineUp", 81 "Down": "goLineDown", 82 "End": "goLineEnd", 83 "Home": "goLineStartSmart", 84 "PageUp": "goPageUp", 85 "PageDown": "goPageDown", 86 "Delete": "delCharAfter", 87 "Backspace": "delCharBefore", 88 "Tab": "defaultTab", 89 "Shift-Tab": "indentLess", 90 "Enter": "smartNewlineAndIndent", 91 "Ctrl-Space": "autocomplete" 92 }; 93 94 CodeMirror.keyMap["devtools-pc"] = { 95 "Ctrl-A": "selectAll", 96 "Ctrl-Z": "undoAndReveal", 97 "Shift-Ctrl-Z": "redoAndReveal", 98 "Ctrl-Y": "redo", 99 "Ctrl-Home": "goDocStart", 100 "Ctrl-Up": "goDocStart", 101 "Ctrl-End": "goDocEnd", 102 "Ctrl-Down": "goDocEnd", 103 "Ctrl-Left": "goGroupLeft", 104 "Ctrl-Right": "goGroupRight", 105 "Alt-Left": "goLineStart", 106 "Alt-Right": "goLineEnd", 107 "Ctrl-Backspace": "delGroupBefore", 108 "Ctrl-Delete": "delGroupAfter", 109 "Ctrl-/": "toggleComment", 110 fallthrough: "devtools-common" 111 }; 112 113 CodeMirror.keyMap["devtools-mac"] = { 114 "Cmd-A" : "selectAll", 115 "Cmd-Z" : "undoAndReveal", 116 "Shift-Cmd-Z": "redoAndReveal", 117 "Cmd-Up": "goDocStart", 118 "Cmd-Down": "goDocEnd", 119 "Alt-Left": "goGroupLeft", 120 "Alt-Right": "goGroupRight", 121 "Cmd-Left": "goLineStartSmart", 122 "Cmd-Right": "goLineEnd", 123 "Alt-Backspace": "delGroupBefore", 124 "Alt-Delete": "delGroupAfter", 125 "Cmd-/": "toggleComment", 126 fallthrough: "devtools-common" 127 }; 128 129 WebInspector.settings.textEditorIndent.addChangeListener(this._updateEditorIndentation, this); 130 this._updateEditorIndentation(); 131 WebInspector.settings.showWhitespacesInEditor.addChangeListener(this._updateCodeMirrorMode, this); 132 133 this._codeMirror.setOption("keyMap", WebInspector.isMac() ? "devtools-mac" : "devtools-pc"); 134 this._codeMirror.setOption("flattenSpans", false); 135 this._codeMirror.setOption("maxHighlightLength", 1000); 136 this._codeMirror.setOption("mode", null); 137 138 this._shouldClearHistory = true; 139 this._lineSeparator = "\n"; 140 141 this._tokenHighlighter = new WebInspector.CodeMirrorTextEditor.TokenHighlighter(this._codeMirror); 142 this._blockIndentController = new WebInspector.CodeMirrorTextEditor.BlockIndentController(this._codeMirror); 143 this._fixWordMovement = new WebInspector.CodeMirrorTextEditor.FixWordMovement(this._codeMirror); 144 this._autocompleteController = new WebInspector.CodeMirrorTextEditor.AutocompleteController(this, this._codeMirror); 145 146 this._codeMirror.on("change", this._change.bind(this)); 147 this._codeMirror.on("beforeChange", this._beforeChange.bind(this)); 148 this._codeMirror.on("gutterClick", this._gutterClick.bind(this)); 149 this._codeMirror.on("cursorActivity", this._cursorActivity.bind(this)); 150 this._codeMirror.on("scroll", this._scroll.bind(this)); 151 this._codeMirror.on("focus", this._focus.bind(this)); 152 this._codeMirror.on("blur", this._blur.bind(this)); 153 this.element.addEventListener("contextmenu", this._contextMenu.bind(this)); 154 155 this.element.addStyleClass("fill"); 156 this.element.style.overflow = "hidden"; 157 this.element.firstChild.addStyleClass("source-code"); 158 this.element.firstChild.addStyleClass("fill"); 159 this._elementToWidget = new Map(); 160 this._nestedUpdatesCounter = 0; 161 162 this.element.addEventListener("focus", this._handleElementFocus.bind(this), false); 163 this.element.addEventListener("keydown", this._handleKeyDown.bind(this), true); 164 this.element.tabIndex = 0; 165 166 this._overrideModeWithPrefixedTokens("css-base", "css-"); 167 this._overrideModeWithPrefixedTokens("javascript", "js-"); 168 this._overrideModeWithPrefixedTokens("xml", "xml-"); 169 170 this._setupSelectionColor(); 171 this._setupWhitespaceHighlight(); 172 } 173 174 WebInspector.CodeMirrorTextEditor.autocompleteCommand = function(codeMirror) 175 { 176 codeMirror._codeMirrorTextEditor._autocompleteController.autocomplete(); 177 } 178 CodeMirror.commands.autocomplete = WebInspector.CodeMirrorTextEditor.autocompleteCommand; 179 180 CodeMirror.commands.smartNewlineAndIndent = function(codeMirror) 181 { 182 codeMirror.operation(innerSmartNewlineAndIndent.bind(this, codeMirror)); 183 184 function countIndent(line) 185 { 186 for(var i = 0; i < line.length; ++i) { 187 if (!WebInspector.TextUtils.isSpaceChar(line[i])) 188 return i; 189 } 190 return line.length; 191 } 192 193 function innerSmartNewlineAndIndent(codeMirror) 194 { 195 var cur = codeMirror.getCursor("start"); 196 var line = codeMirror.getLine(cur.line); 197 var indent = cur.line > 0 ? countIndent(line) : 0; 198 if (cur.ch <= indent) { 199 codeMirror.replaceSelection("\n" + line.substring(0, cur.ch), "end", "+input"); 200 codeMirror.setSelection(new CodeMirror.Pos(cur.line + 1, cur.ch)); 201 } else 202 codeMirror.execCommand("newlineAndIndent"); 203 } 204 } 205 206 CodeMirror.commands.undoAndReveal = function(codemirror) 207 { 208 var scrollInfo = codemirror.getScrollInfo(); 209 codemirror.execCommand("undo"); 210 var cursor = codemirror.getCursor("start"); 211 codemirror._codeMirrorTextEditor._innerRevealLine(cursor.line, scrollInfo); 212 } 213 214 CodeMirror.commands.redoAndReveal = function(codemirror) 215 { 216 var scrollInfo = codemirror.getScrollInfo(); 217 codemirror.execCommand("redo"); 218 var cursor = codemirror.getCursor("start"); 219 codemirror._codeMirrorTextEditor._innerRevealLine(cursor.line, scrollInfo); 220 } 221 222 WebInspector.CodeMirrorTextEditor.LongLineModeLineLengthThreshold = 2000; 223 WebInspector.CodeMirrorTextEditor.MaximumNumberOfWhitespacesPerSingleSpan = 16; 224 225 WebInspector.CodeMirrorTextEditor.prototype = { 226 wasShown: function() 227 { 228 this._codeMirror.refresh(); 229 }, 230 231 _guessIndentationLevel: function() 232 { 233 var tabRegex = /^\t+/; 234 var tabLines = 0; 235 var indents = {}; 236 function processLine(lineHandle) 237 { 238 var text = lineHandle.text; 239 if (text.length === 0 || !WebInspector.TextUtils.isSpaceChar(text[0])) 240 return; 241 if (tabRegex.test(text)) { 242 ++tabLines; 243 return; 244 } 245 var i = 0; 246 while (i < text.length && WebInspector.TextUtils.isSpaceChar(text[i])) 247 ++i; 248 if (i % 2 !== 0) 249 return; 250 indents[i] = 1 + (indents[i] || 0); 251 } 252 this._codeMirror.eachLine(processLine); 253 254 var onePercentFilterThreshold = this.linesCount / 100; 255 if (tabLines && tabLines > onePercentFilterThreshold) 256 return "\t"; 257 var minimumIndent = Infinity; 258 for (var i in indents) { 259 if (indents[i] < onePercentFilterThreshold) 260 continue; 261 var indent = parseInt(i, 10); 262 if (minimumIndent > indent) 263 minimumIndent = indent; 264 } 265 if (minimumIndent === Infinity) 266 return WebInspector.TextUtils.Indent.FourSpaces; 267 return new Array(minimumIndent + 1).join(" "); 268 }, 269 270 _updateEditorIndentation: function() 271 { 272 var extraKeys = {}; 273 var indent = WebInspector.settings.textEditorIndent.get(); 274 if (WebInspector.settings.textEditorAutoDetectIndent.get()) 275 indent = this._guessIndentationLevel(); 276 if (indent === WebInspector.TextUtils.Indent.TabCharacter) { 277 this._codeMirror.setOption("indentWithTabs", true); 278 this._codeMirror.setOption("indentUnit", 4); 279 } else { 280 this._codeMirror.setOption("indentWithTabs", false); 281 this._codeMirror.setOption("indentUnit", indent.length); 282 extraKeys.Tab = function(codeMirror) 283 { 284 if (codeMirror.somethingSelected()) 285 return CodeMirror.Pass; 286 var pos = codeMirror.getCursor("head"); 287 codeMirror.replaceRange(indent.substring(pos.ch % indent.length), codeMirror.getCursor()); 288 } 289 } 290 this._codeMirror.setOption("extraKeys", extraKeys); 291 this._indentationLevel = indent; 292 }, 293 294 /** 295 * @return {string} 296 */ 297 indent: function() 298 { 299 return this._indentationLevel; 300 }, 301 302 /** 303 * @param {!RegExp} regex 304 * @param {WebInspector.TextRange} range 305 */ 306 highlightSearchResults: function(regex, range) 307 { 308 function innerHighlightRegex() 309 { 310 if (range) { 311 this.revealLine(range.startLine); 312 this.setSelection(WebInspector.TextRange.createFromLocation(range.startLine, range.startColumn)); 313 } else { 314 // Collapse selection to end on search start so that we jump to next occurence on the first enter press. 315 this.setSelection(this.selection().collapseToEnd()); 316 } 317 this._tokenHighlighter.highlightSearchResults(regex, range); 318 } 319 320 this._codeMirror.operation(innerHighlightRegex.bind(this)); 321 }, 322 323 cancelSearchResultsHighlight: function() 324 { 325 this._codeMirror.operation(this._tokenHighlighter.highlightSelectedTokens.bind(this._tokenHighlighter)); 326 }, 327 328 undo: function() 329 { 330 this._codeMirror.undo(); 331 }, 332 333 redo: function() 334 { 335 this._codeMirror.redo(); 336 }, 337 338 _setupSelectionColor: function() 339 { 340 if (WebInspector.CodeMirrorTextEditor._selectionStyleInjected) 341 return; 342 WebInspector.CodeMirrorTextEditor._selectionStyleInjected = true; 343 var backgroundColor = WebInspector.getSelectionBackgroundColor(); 344 var backgroundColorRule = backgroundColor ? ".CodeMirror .CodeMirror-selected { background-color: " + backgroundColor + ";}" : ""; 345 var foregroundColor = WebInspector.getSelectionForegroundColor(); 346 var foregroundColorRule = foregroundColor ? ".CodeMirror .CodeMirror-selectedtext:not(.CodeMirror-persist-highlight) { color: " + foregroundColor + "!important;}" : ""; 347 if (!foregroundColorRule && !backgroundColorRule) 348 return; 349 350 var style = document.createElement("style"); 351 style.textContent = backgroundColorRule + foregroundColorRule; 352 document.head.appendChild(style); 353 }, 354 355 _setupWhitespaceHighlight: function() 356 { 357 if (WebInspector.CodeMirrorTextEditor._whitespaceStyleInjected || !WebInspector.settings.showWhitespacesInEditor.get()) 358 return; 359 WebInspector.CodeMirrorTextEditor._whitespaceStyleInjected = true; 360 const classBase = ".cm-whitespace-"; 361 const spaceChar = ""; 362 var spaceChars = ""; 363 var rules = ""; 364 for(var i = 1; i <= WebInspector.CodeMirrorTextEditor.MaximumNumberOfWhitespacesPerSingleSpan; ++i) { 365 spaceChars += spaceChar; 366 var rule = classBase + i + "::before { content: '" + spaceChars + "';}\n"; 367 rules += rule; 368 } 369 rules += ".cm-tab:before { display: block !important; }\n"; 370 var style = document.createElement("style"); 371 style.textContent = rules; 372 document.head.appendChild(style); 373 }, 374 375 _handleKeyDown: function(e) 376 { 377 if (this._autocompleteController.keyDown(e)) 378 e.consume(true); 379 }, 380 381 _shouldProcessWordForAutocompletion: function(word) 382 { 383 return word.length && (word[0] < '0' || word[0] > '9'); 384 }, 385 386 /** 387 * @param {string} text 388 */ 389 _addTextToCompletionDictionary: function(text) 390 { 391 var words = WebInspector.TextUtils.textToWords(text); 392 for(var i = 0; i < words.length; ++i) { 393 if (this._shouldProcessWordForAutocompletion(words[i])) 394 this._dictionary.addWord(words[i]); 395 } 396 }, 397 398 /** 399 * @param {string} text 400 */ 401 _removeTextFromCompletionDictionary: function(text) 402 { 403 var words = WebInspector.TextUtils.textToWords(text); 404 for(var i = 0; i < words.length; ++i) { 405 if (this._shouldProcessWordForAutocompletion(words[i])) 406 this._dictionary.removeWord(words[i]); 407 } 408 }, 409 410 /** 411 * @param {WebInspector.CompletionDictionary} dictionary 412 */ 413 setCompletionDictionary: function(dictionary) 414 { 415 this._dictionary = dictionary; 416 this._addTextToCompletionDictionary(this.text()); 417 }, 418 419 /** 420 * @param {number} lineNumber 421 * @param {number} column 422 * @return {?{x: number, y: number, height: number}} 423 */ 424 cursorPositionToCoordinates: function(lineNumber, column) 425 { 426 if (lineNumber >= this._codeMirror.lineCount() || lineNumber < 0 || column < 0 || column > this._codeMirror.getLine(lineNumber).length) 427 return null; 428 429 var metrics = this._codeMirror.cursorCoords(new CodeMirror.Pos(lineNumber, column)); 430 431 return { 432 x: metrics.left, 433 y: metrics.top, 434 height: metrics.bottom - metrics.top 435 }; 436 }, 437 438 /** 439 * @param {number} x 440 * @param {number} y 441 * @return {?WebInspector.TextRange} 442 */ 443 coordinatesToCursorPosition: function(x, y) 444 { 445 var element = document.elementFromPoint(x, y); 446 if (!element || !element.isSelfOrDescendant(this._codeMirror.getWrapperElement())) 447 return null; 448 var gutterBox = this._codeMirror.getGutterElement().boxInWindow(); 449 if (x >= gutterBox.x && x <= gutterBox.x + gutterBox.width && 450 y >= gutterBox.y && y <= gutterBox.y + gutterBox.height) 451 return null; 452 var coords = this._codeMirror.coordsChar({left: x, top: y}); 453 return this._toRange(coords, coords); 454 }, 455 456 _convertTokenType: function(tokenType) 457 { 458 if (tokenType.startsWith("js-variable") || tokenType.startsWith("js-property") || tokenType === "js-def") 459 return "javascript-ident"; 460 if (tokenType === "js-string-2") 461 return "javascript-regexp"; 462 if (tokenType === "js-number" || tokenType === "js-comment" || tokenType === "js-string" || tokenType === "js-keyword") 463 return "javascript-" + tokenType.substring("js-".length); 464 return null; 465 }, 466 467 /** 468 * @param {number} lineNumber 469 * @param {number} column 470 * @return {?{startColumn: number, endColumn: number, type: string}} 471 */ 472 tokenAtTextPosition: function(lineNumber, column) 473 { 474 if (lineNumber < 0 || lineNumber >= this._codeMirror.lineCount()) 475 return null; 476 var token = this._codeMirror.getTokenAt(new CodeMirror.Pos(lineNumber, (column || 0) + 1)); 477 if (!token || !token.type) 478 return null; 479 var convertedType = this._convertTokenType(token.type); 480 if (!convertedType) 481 return null; 482 return { 483 startColumn: token.start, 484 endColumn: token.end - 1, 485 type: convertedType 486 }; 487 }, 488 489 /** 490 * @param {WebInspector.TextRange} textRange 491 * @return {string} 492 */ 493 copyRange: function(textRange) 494 { 495 var pos = this._toPos(textRange); 496 return this._codeMirror.getRange(pos.start, pos.end); 497 }, 498 499 /** 500 * @return {boolean} 501 */ 502 isClean: function() 503 { 504 return this._codeMirror.isClean(); 505 }, 506 507 markClean: function() 508 { 509 this._codeMirror.markClean(); 510 }, 511 512 _hasLongLines: function() 513 { 514 function lineIterator(lineHandle) 515 { 516 if (lineHandle.text.length > WebInspector.CodeMirrorTextEditor.LongLineModeLineLengthThreshold) 517 hasLongLines = true; 518 return hasLongLines; 519 } 520 var hasLongLines = false; 521 this._codeMirror.eachLine(lineIterator); 522 return hasLongLines; 523 }, 524 525 /** 526 * @param {string} mimeType 527 * @return {string} 528 */ 529 _whitespaceOverlayMode: function(mimeType) 530 { 531 var modeName = CodeMirror.mimeModes[mimeType] + "+whitespaces"; 532 if (CodeMirror.modes[modeName]) 533 return modeName; 534 535 function modeConstructor(config, parserConfig) 536 { 537 function nextToken(stream) 538 { 539 if (stream.peek() === " ") { 540 var spaces = 0; 541 while (spaces < WebInspector.CodeMirrorTextEditor.MaximumNumberOfWhitespacesPerSingleSpan && stream.peek() === " ") { 542 ++spaces; 543 stream.next(); 544 } 545 return "whitespace whitespace-" + spaces; 546 } 547 while (!stream.eol() && stream.peek() !== " ") 548 stream.next(); 549 return null; 550 } 551 var whitespaceMode = { 552 token: nextToken 553 }; 554 return CodeMirror.overlayMode(CodeMirror.getMode(config, mimeType), whitespaceMode, false); 555 } 556 CodeMirror.defineMode(modeName, modeConstructor); 557 return modeName; 558 }, 559 560 /** 561 * @param {string} modeName 562 * @param {string} tokenPrefix 563 */ 564 _overrideModeWithPrefixedTokens: function(modeName, tokenPrefix) 565 { 566 var oldModeName = modeName + "-old"; 567 if (CodeMirror.modes[oldModeName]) 568 return; 569 570 CodeMirror.defineMode(oldModeName, CodeMirror.modes[modeName]); 571 CodeMirror.defineMode(modeName, modeConstructor); 572 573 function modeConstructor(config, parserConfig) 574 { 575 var innerConfig = {}; 576 for (var i in parserConfig) 577 innerConfig[i] = parserConfig[i]; 578 innerConfig.name = oldModeName; 579 var codeMirrorMode = CodeMirror.getMode(config, innerConfig); 580 codeMirrorMode.name = modeName; 581 codeMirrorMode.token = tokenOverride.bind(this, codeMirrorMode.token); 582 return codeMirrorMode; 583 } 584 585 function tokenOverride(superToken, stream, state) 586 { 587 var token = superToken(stream, state); 588 return token ? tokenPrefix + token : token; 589 } 590 }, 591 592 _enableLongLinesMode: function() 593 { 594 this._codeMirror.setOption("styleSelectedText", false); 595 this._longLinesMode = true; 596 }, 597 598 _disableLongLinesMode: function() 599 { 600 this._codeMirror.setOption("styleSelectedText", true); 601 this._longLinesMode = false; 602 }, 603 604 _updateCodeMirrorMode: function() 605 { 606 var showWhitespaces = WebInspector.settings.showWhitespacesInEditor.get(); 607 this._codeMirror.setOption("mode", showWhitespaces ? this._whitespaceOverlayMode(this._mimeType) : this._mimeType); 608 }, 609 610 /** 611 * @param {string} mimeType 612 */ 613 setMimeType: function(mimeType) 614 { 615 this._mimeType = mimeType; 616 if (this._hasLongLines()) 617 this._enableLongLinesMode(); 618 else 619 this._disableLongLinesMode(); 620 this._updateCodeMirrorMode(); 621 }, 622 623 /** 624 * @param {boolean} readOnly 625 */ 626 setReadOnly: function(readOnly) 627 { 628 this.element.enableStyleClass("CodeMirror-readonly", readOnly) 629 this._codeMirror.setOption("readOnly", readOnly); 630 }, 631 632 /** 633 * @return {boolean} 634 */ 635 readOnly: function() 636 { 637 return !!this._codeMirror.getOption("readOnly"); 638 }, 639 640 /** 641 * @param {Object} highlightDescriptor 642 */ 643 removeHighlight: function(highlightDescriptor) 644 { 645 highlightDescriptor.clear(); 646 }, 647 648 /** 649 * @param {WebInspector.TextRange} range 650 * @param {string} cssClass 651 * @return {Object} 652 */ 653 highlightRange: function(range, cssClass) 654 { 655 cssClass = "CodeMirror-persist-highlight " + cssClass; 656 var pos = this._toPos(range); 657 ++pos.end.ch; 658 return this._codeMirror.markText(pos.start, pos.end, { 659 className: cssClass, 660 startStyle: cssClass + "-start", 661 endStyle: cssClass + "-end" 662 }); 663 }, 664 665 /** 666 * @param {string} regex 667 * @param {string} cssClass 668 * @return {Object} 669 */ 670 highlightRegex: function(regex, cssClass) { }, 671 672 /** 673 * @return {Element} 674 */ 675 defaultFocusedElement: function() 676 { 677 return this.element; 678 }, 679 680 focus: function() 681 { 682 this._codeMirror.focus(); 683 }, 684 685 _handleElementFocus: function() 686 { 687 this._codeMirror.focus(); 688 }, 689 690 beginUpdates: function() 691 { 692 ++this._nestedUpdatesCounter; 693 }, 694 695 endUpdates: function() 696 { 697 if (!--this._nestedUpdatesCounter) 698 this._codeMirror.refresh(); 699 }, 700 701 /** 702 * @param {number} lineNumber 703 */ 704 revealLine: function(lineNumber) 705 { 706 this._innerRevealLine(lineNumber, this._codeMirror.getScrollInfo()); 707 }, 708 709 /** 710 * @param {number} lineNumber 711 * @param {{left: number, top: number, width: number, height: number, clientWidth: number, clientHeight: number}} scrollInfo 712 */ 713 _innerRevealLine: function(lineNumber, scrollInfo) 714 { 715 var topLine = this._codeMirror.lineAtHeight(scrollInfo.top, "local"); 716 var bottomLine = this._codeMirror.lineAtHeight(scrollInfo.top + scrollInfo.clientHeight, "local"); 717 var linesPerScreen = bottomLine - topLine + 1; 718 if (lineNumber < topLine) { 719 var topLineToReveal = Math.max(lineNumber - (linesPerScreen / 2) + 1, 0) | 0; 720 this._codeMirror.scrollIntoView(new CodeMirror.Pos(topLineToReveal, 0)); 721 } else if (lineNumber > bottomLine) { 722 var bottomLineToReveal = Math.min(lineNumber + (linesPerScreen / 2) - 1, this.linesCount - 1) | 0; 723 this._codeMirror.scrollIntoView(new CodeMirror.Pos(bottomLineToReveal, 0)); 724 } 725 }, 726 727 _gutterClick: function(instance, lineNumber, gutter, event) 728 { 729 this.dispatchEventToListeners(WebInspector.TextEditor.Events.GutterClick, { lineNumber: lineNumber, event: event }); 730 }, 731 732 _contextMenu: function(event) 733 { 734 var contextMenu = new WebInspector.ContextMenu(event); 735 var target = event.target.enclosingNodeOrSelfWithClass("CodeMirror-gutter-elt"); 736 if (target) 737 this._delegate.populateLineGutterContextMenu(contextMenu, parseInt(target.textContent, 10) - 1); 738 else 739 this._delegate.populateTextAreaContextMenu(contextMenu, 0); 740 contextMenu.show(); 741 }, 742 743 /** 744 * @param {number} lineNumber 745 * @param {boolean} disabled 746 * @param {boolean} conditional 747 */ 748 addBreakpoint: function(lineNumber, disabled, conditional) 749 { 750 if (lineNumber < 0 || lineNumber >= this._codeMirror.lineCount()) 751 return; 752 var className = "cm-breakpoint" + (conditional ? " cm-breakpoint-conditional" : "") + (disabled ? " cm-breakpoint-disabled" : ""); 753 this._codeMirror.addLineClass(lineNumber, "wrap", className); 754 }, 755 756 /** 757 * @param {number} lineNumber 758 */ 759 removeBreakpoint: function(lineNumber) 760 { 761 if (lineNumber < 0 || lineNumber >= this._codeMirror.lineCount()) 762 return; 763 var wrapClasses = this._codeMirror.getLineHandle(lineNumber).wrapClass; 764 if (!wrapClasses) 765 return; 766 var classes = wrapClasses.split(" "); 767 for(var i = 0; i < classes.length; ++i) { 768 if (classes[i].startsWith("cm-breakpoint")) 769 this._codeMirror.removeLineClass(lineNumber, "wrap", classes[i]); 770 } 771 }, 772 773 /** 774 * @param {number} lineNumber 775 */ 776 setExecutionLine: function(lineNumber) 777 { 778 this._executionLine = this._codeMirror.getLineHandle(lineNumber); 779 this._codeMirror.addLineClass(this._executionLine, "wrap", "cm-execution-line"); 780 }, 781 782 clearExecutionLine: function() 783 { 784 if (this._executionLine) 785 this._codeMirror.removeLineClass(this._executionLine, "wrap", "cm-execution-line"); 786 delete this._executionLine; 787 }, 788 789 /** 790 * @param {number} lineNumber 791 * @param {Element} element 792 */ 793 addDecoration: function(lineNumber, element) 794 { 795 var widget = this._codeMirror.addLineWidget(lineNumber, element); 796 this._elementToWidget.put(element, widget); 797 }, 798 799 /** 800 * @param {number} lineNumber 801 * @param {Element} element 802 */ 803 removeDecoration: function(lineNumber, element) 804 { 805 var widget = this._elementToWidget.remove(element); 806 if (widget) 807 this._codeMirror.removeLineWidget(widget); 808 }, 809 810 /** 811 * @param {number} lineNumber 812 * @param {number=} columnNumber 813 */ 814 highlightPosition: function(lineNumber, columnNumber) 815 { 816 if (lineNumber < 0) 817 return; 818 lineNumber = Math.min(lineNumber, this._codeMirror.lineCount() - 1); 819 if (typeof columnNumber !== "number" || columnNumber < 0 || columnNumber > this._codeMirror.getLine(lineNumber).length) 820 columnNumber = 0; 821 822 this.clearPositionHighlight(); 823 this._highlightedLine = this._codeMirror.getLineHandle(lineNumber); 824 if (!this._highlightedLine) 825 return; 826 this.revealLine(lineNumber); 827 this._codeMirror.addLineClass(this._highlightedLine, null, "cm-highlight"); 828 this._clearHighlightTimeout = setTimeout(this.clearPositionHighlight.bind(this), 2000); 829 if (!this.readOnly()) 830 this._codeMirror.setSelection(new CodeMirror.Pos(lineNumber, columnNumber)); 831 }, 832 833 clearPositionHighlight: function() 834 { 835 if (this._clearHighlightTimeout) 836 clearTimeout(this._clearHighlightTimeout); 837 delete this._clearHighlightTimeout; 838 839 if (this._highlightedLine) 840 this._codeMirror.removeLineClass(this._highlightedLine, null, "cm-highlight"); 841 delete this._highlightedLine; 842 }, 843 844 /** 845 * @return {Array.<Element>} 846 */ 847 elementsToRestoreScrollPositionsFor: function() 848 { 849 return []; 850 }, 851 852 /** 853 * @param {WebInspector.TextEditor} textEditor 854 */ 855 inheritScrollPositions: function(textEditor) 856 { 857 }, 858 859 _updatePaddingBottom: function(width, height) 860 { 861 var scrollInfo = this._codeMirror.getScrollInfo(); 862 var newPaddingBottom; 863 var linesElement = this.element.firstChild.querySelector(".CodeMirror-lines"); 864 var lineCount = this._codeMirror.lineCount(); 865 if (lineCount <= 1) 866 newPaddingBottom = 0; 867 else 868 newPaddingBottom = Math.max(scrollInfo.clientHeight - this._codeMirror.getLineHandle(this._codeMirror.lastLine()).height, 0); 869 newPaddingBottom += "px"; 870 linesElement.style.paddingBottom = newPaddingBottom; 871 this._codeMirror.setSize(width, height); 872 }, 873 874 _resizeEditor: function() 875 { 876 var scrollInfo = this._codeMirror.getScrollInfo(); 877 var width = this.element.parentElement.offsetWidth; 878 var height = this.element.parentElement.offsetHeight; 879 this._codeMirror.setSize(width, height); 880 this._updatePaddingBottom(width, height); 881 this._codeMirror.scrollTo(scrollInfo.left, scrollInfo.top); 882 }, 883 884 onResize: function() 885 { 886 if (WebInspector.experimentsSettings.scrollBeyondEndOfFile.isEnabled()) 887 this._resizeEditor(); 888 else { 889 var width = this.element.parentElement.offsetWidth; 890 var height = this.element.parentElement.offsetHeight; 891 this._codeMirror.setSize(width, height); 892 } 893 }, 894 895 /** 896 * @param {WebInspector.TextRange} range 897 * @param {string} text 898 * @return {WebInspector.TextRange} 899 */ 900 editRange: function(range, text) 901 { 902 var pos = this._toPos(range); 903 this._codeMirror.replaceRange(text, pos.start, pos.end); 904 var newRange = this._toRange(pos.start, this._codeMirror.posFromIndex(this._codeMirror.indexFromPos(pos.start) + text.length)); 905 this._delegate.onTextChanged(range, newRange); 906 if (WebInspector.settings.textEditorAutoDetectIndent.get()) 907 this._updateEditorIndentation(); 908 return newRange; 909 }, 910 911 /** 912 * @param {number} lineNumber 913 * @param {number} column 914 * @param {boolean=} prefixOnly 915 * @return {?WebInspector.TextRange} 916 */ 917 _wordRangeForCursorPosition: function(lineNumber, column, prefixOnly) 918 { 919 var line = this.line(lineNumber); 920 if (column === 0 || !WebInspector.TextUtils.isWordChar(line.charAt(column - 1))) 921 return null; 922 var wordStart = column - 1; 923 while(wordStart > 0 && WebInspector.TextUtils.isWordChar(line.charAt(wordStart - 1))) 924 --wordStart; 925 if (prefixOnly) 926 return new WebInspector.TextRange(lineNumber, wordStart, lineNumber, column); 927 var wordEnd = column; 928 while(wordEnd < line.length && WebInspector.TextUtils.isWordChar(line.charAt(wordEnd))) 929 ++wordEnd; 930 return new WebInspector.TextRange(lineNumber, wordStart, lineNumber, wordEnd); 931 }, 932 933 _beforeChange: function(codeMirror, changeObject) 934 { 935 if (!this._dictionary) 936 return; 937 this._updatedLines = this._updatedLines || {}; 938 for(var i = changeObject.from.line; i <= changeObject.to.line; ++i) 939 this._updatedLines[i] = this.line(i); 940 }, 941 942 /** 943 * @param {CodeMirror} codeMirror 944 * @param {{origin: string, text: Array.<string>, removed: Array.<string>}} changeObject 945 */ 946 _change: function(codeMirror, changeObject) 947 { 948 if (WebInspector.experimentsSettings.scrollBeyondEndOfFile.isEnabled()) { 949 var hasOneLine = this._codeMirror.lineCount() === 1; 950 if (hasOneLine !== this._hasOneLine) 951 this._resizeEditor(); 952 this._hasOneLine = hasOneLine; 953 } 954 var widgets = this._elementToWidget.values(); 955 for (var i = 0; i < widgets.length; ++i) 956 this._codeMirror.removeLineWidget(widgets[i]); 957 this._elementToWidget.clear(); 958 959 if (this._updatedLines) { 960 for(var lineNumber in this._updatedLines) 961 this._removeTextFromCompletionDictionary(this._updatedLines[lineNumber]); 962 delete this._updatedLines; 963 } 964 965 var linesToUpdate = {}; 966 var singleCharInput = false; 967 do { 968 var oldRange = this._toRange(changeObject.from, changeObject.to); 969 var newRange = oldRange.clone(); 970 var linesAdded = changeObject.text.length; 971 singleCharInput = (changeObject.origin === "+input" && changeObject.text.length === 1 && changeObject.text[0].length === 1) || 972 (changeObject.origin === "+delete" && changeObject.removed.length === 1 && changeObject.removed[0].length === 1); 973 if (linesAdded === 0) { 974 newRange.endLine = newRange.startLine; 975 newRange.endColumn = newRange.startColumn; 976 } else if (linesAdded === 1) { 977 newRange.endLine = newRange.startLine; 978 newRange.endColumn = newRange.startColumn + changeObject.text[0].length; 979 } else { 980 newRange.endLine = newRange.startLine + linesAdded - 1; 981 newRange.endColumn = changeObject.text[linesAdded - 1].length; 982 } 983 984 if (!this._muteTextChangedEvent) 985 this._delegate.onTextChanged(oldRange, newRange); 986 987 for(var i = newRange.startLine; i <= newRange.endLine; ++i) { 988 linesToUpdate[i] = true; 989 } 990 if (this._dictionary) { 991 for(var i = newRange.startLine; i <= newRange.endLine; ++i) 992 linesToUpdate[i] = this.line(i); 993 } 994 } while (changeObject = changeObject.next); 995 if (this._dictionary) { 996 for(var lineNumber in linesToUpdate) 997 this._addTextToCompletionDictionary(linesToUpdate[lineNumber]); 998 } 999 if (singleCharInput) 1000 this._autocompleteController.autocomplete(); 1001 }, 1002 1003 _cursorActivity: function() 1004 { 1005 var start = this._codeMirror.getCursor("anchor"); 1006 var end = this._codeMirror.getCursor("head"); 1007 this._delegate.selectionChanged(this._toRange(start, end)); 1008 if (!this._tokenHighlighter.highlightedRegex()) 1009 this._codeMirror.operation(this._tokenHighlighter.highlightSelectedTokens.bind(this._tokenHighlighter)); 1010 }, 1011 1012 _scroll: function() 1013 { 1014 if (this._scrollTimer) 1015 clearTimeout(this._scrollTimer); 1016 var topmostLineNumber = this._codeMirror.lineAtHeight(this._codeMirror.getScrollInfo().top, "local"); 1017 this._scrollTimer = setTimeout(this._delegate.scrollChanged.bind(this._delegate, topmostLineNumber), 100); 1018 }, 1019 1020 _focus: function() 1021 { 1022 this._delegate.editorFocused(); 1023 }, 1024 1025 _blur: function() 1026 { 1027 this._autocompleteController.finishAutocomplete(); 1028 }, 1029 1030 /** 1031 * @param {number} lineNumber 1032 */ 1033 scrollToLine: function(lineNumber) 1034 { 1035 var pos = new CodeMirror.Pos(lineNumber, 0); 1036 var coords = this._codeMirror.charCoords(pos, "local"); 1037 this._codeMirror.scrollTo(0, coords.top); 1038 }, 1039 1040 /** 1041 * @return {number} 1042 */ 1043 firstVisibleLine: function() 1044 { 1045 return this._codeMirror.lineAtHeight(this._codeMirror.getScrollInfo().top, "local"); 1046 }, 1047 1048 /** 1049 * @return {number} 1050 */ 1051 lastVisibleLine: function() 1052 { 1053 var scrollInfo = this._codeMirror.getScrollInfo(); 1054 return this._codeMirror.lineAtHeight(scrollInfo.top + scrollInfo.clientHeight, "local"); 1055 }, 1056 1057 /** 1058 * @return {WebInspector.TextRange} 1059 */ 1060 selection: function() 1061 { 1062 var start = this._codeMirror.getCursor("anchor"); 1063 var end = this._codeMirror.getCursor("head"); 1064 1065 return this._toRange(start, end); 1066 }, 1067 1068 /** 1069 * @return {WebInspector.TextRange?} 1070 */ 1071 lastSelection: function() 1072 { 1073 return this._lastSelection; 1074 }, 1075 1076 /** 1077 * @param {WebInspector.TextRange} textRange 1078 */ 1079 setSelection: function(textRange) 1080 { 1081 this._lastSelection = textRange; 1082 var pos = this._toPos(textRange); 1083 this._codeMirror.setSelection(pos.start, pos.end); 1084 }, 1085 1086 /** 1087 * @param {string} text 1088 */ 1089 _detectLineSeparator: function(text) 1090 { 1091 this._lineSeparator = text.indexOf("\r\n") >= 0 ? "\r\n" : "\n"; 1092 }, 1093 1094 /** 1095 * @param {string} text 1096 */ 1097 setText: function(text) 1098 { 1099 this._muteTextChangedEvent = true; 1100 this._codeMirror.setValue(text); 1101 this._updateEditorIndentation(); 1102 if (this._shouldClearHistory) { 1103 this._codeMirror.clearHistory(); 1104 this._shouldClearHistory = false; 1105 } 1106 this._detectLineSeparator(text); 1107 delete this._muteTextChangedEvent; 1108 }, 1109 1110 /** 1111 * @return {string} 1112 */ 1113 text: function() 1114 { 1115 return this._codeMirror.getValue().replace(/\n/g, this._lineSeparator); 1116 }, 1117 1118 /** 1119 * @return {WebInspector.TextRange} 1120 */ 1121 range: function() 1122 { 1123 var lineCount = this.linesCount; 1124 var lastLine = this._codeMirror.getLine(lineCount - 1); 1125 return this._toRange(new CodeMirror.Pos(0, 0), new CodeMirror.Pos(lineCount - 1, lastLine.length)); 1126 }, 1127 1128 /** 1129 * @param {number} lineNumber 1130 * @return {string} 1131 */ 1132 line: function(lineNumber) 1133 { 1134 return this._codeMirror.getLine(lineNumber); 1135 }, 1136 1137 /** 1138 * @return {number} 1139 */ 1140 get linesCount() 1141 { 1142 return this._codeMirror.lineCount(); 1143 }, 1144 1145 /** 1146 * @param {number} line 1147 * @param {string} name 1148 * @param {Object?} value 1149 */ 1150 setAttribute: function(line, name, value) 1151 { 1152 if (line < 0 || line >= this._codeMirror.lineCount()) 1153 return; 1154 var handle = this._codeMirror.getLineHandle(line); 1155 if (handle.attributes === undefined) handle.attributes = {}; 1156 handle.attributes[name] = value; 1157 }, 1158 1159 /** 1160 * @param {number} line 1161 * @param {string} name 1162 * @return {?Object} value 1163 */ 1164 getAttribute: function(line, name) 1165 { 1166 if (line < 0 || line >= this._codeMirror.lineCount()) 1167 return null; 1168 var handle = this._codeMirror.getLineHandle(line); 1169 return handle.attributes && handle.attributes[name] !== undefined ? handle.attributes[name] : null; 1170 }, 1171 1172 /** 1173 * @param {number} line 1174 * @param {string} name 1175 */ 1176 removeAttribute: function(line, name) 1177 { 1178 if (line < 0 || line >= this._codeMirror.lineCount()) 1179 return; 1180 var handle = this._codeMirror.getLineHandle(line); 1181 if (handle && handle.attributes) 1182 delete handle.attributes[name]; 1183 }, 1184 1185 /** 1186 * @param {WebInspector.TextRange} range 1187 * @return {{start: CodeMirror.Pos, end: CodeMirror.Pos}} 1188 */ 1189 _toPos: function(range) 1190 { 1191 return { 1192 start: new CodeMirror.Pos(range.startLine, range.startColumn), 1193 end: new CodeMirror.Pos(range.endLine, range.endColumn) 1194 } 1195 }, 1196 1197 _toRange: function(start, end) 1198 { 1199 return new WebInspector.TextRange(start.line, start.ch, end.line, end.ch); 1200 }, 1201 1202 __proto__: WebInspector.View.prototype 1203 } 1204 1205 /** 1206 * @constructor 1207 * @param {CodeMirror} codeMirror 1208 */ 1209 WebInspector.CodeMirrorTextEditor.TokenHighlighter = function(codeMirror) 1210 { 1211 this._codeMirror = codeMirror; 1212 } 1213 1214 WebInspector.CodeMirrorTextEditor.TokenHighlighter.prototype = { 1215 /** 1216 * @param {RegExp} regex 1217 * @param {WebInspector.TextRange} range 1218 */ 1219 highlightSearchResults: function(regex, range) 1220 { 1221 var oldRegex = this._highlightRegex; 1222 this._highlightRegex = regex; 1223 this._highlightRange = range; 1224 if (this._searchResultMarker) { 1225 this._searchResultMarker.clear(); 1226 delete this._searchResultMarker; 1227 } 1228 if (this._highlightDescriptor && this._highlightDescriptor.selectionStart) 1229 this._codeMirror.removeLineClass(this._highlightDescriptor.selectionStart.line, "wrap", "cm-line-with-selection"); 1230 var selectionStart = this._highlightRange ? new CodeMirror.Pos(this._highlightRange.startLine, this._highlightRange.startColumn) : null; 1231 if (selectionStart) 1232 this._codeMirror.addLineClass(selectionStart.line, "wrap", "cm-line-with-selection"); 1233 if (this._highlightRegex === oldRegex) { 1234 // Do not re-add overlay mode if regex did not change for better performance. 1235 if (this._highlightDescriptor) 1236 this._highlightDescriptor.selectionStart = selectionStart; 1237 } else { 1238 this._removeHighlight(); 1239 this._setHighlighter(this._searchHighlighter.bind(this, this._highlightRegex, this._highlightRange), selectionStart); 1240 } 1241 if (selectionStart) { 1242 var pos = WebInspector.CodeMirrorTextEditor.prototype._toPos(this._highlightRange); 1243 this._searchResultMarker = this._codeMirror.markText(pos.start, pos.end, {className: "cm-column-with-selection"}); 1244 } 1245 }, 1246 1247 highlightedRegex: function() 1248 { 1249 return this._highlightRegex; 1250 }, 1251 1252 highlightSelectedTokens: function() 1253 { 1254 delete this._highlightRegex; 1255 delete this._highlightRange; 1256 1257 if (this._highlightDescriptor && this._highlightDescriptor.selectionStart) 1258 this._codeMirror.removeLineClass(this._highlightDescriptor.selectionStart.line, "wrap", "cm-line-with-selection"); 1259 this._removeHighlight(); 1260 var selectionStart = this._codeMirror.getCursor("start"); 1261 var selectionEnd = this._codeMirror.getCursor("end"); 1262 if (selectionStart.line !== selectionEnd.line) 1263 return; 1264 if (selectionStart.ch === selectionEnd.ch) 1265 return; 1266 1267 var selectedText = this._codeMirror.getSelection(); 1268 if (this._isWord(selectedText, selectionStart.line, selectionStart.ch, selectionEnd.ch)) { 1269 if (selectionStart) 1270 this._codeMirror.addLineClass(selectionStart.line, "wrap", "cm-line-with-selection") 1271 this._setHighlighter(this._tokenHighlighter.bind(this, selectedText, selectionStart), selectionStart); 1272 } 1273 }, 1274 1275 /** 1276 * @param {string} selectedText 1277 * @param {number} lineNumber 1278 * @param {number} startColumn 1279 * @param {number} endColumn 1280 */ 1281 _isWord: function(selectedText, lineNumber, startColumn, endColumn) 1282 { 1283 var line = this._codeMirror.getLine(lineNumber); 1284 var leftBound = startColumn === 0 || !WebInspector.TextUtils.isWordChar(line.charAt(startColumn - 1)); 1285 var rightBound = endColumn === line.length || !WebInspector.TextUtils.isWordChar(line.charAt(endColumn)); 1286 return leftBound && rightBound && WebInspector.TextUtils.isWord(selectedText); 1287 }, 1288 1289 _removeHighlight: function() 1290 { 1291 if (this._highlightDescriptor) { 1292 this._codeMirror.removeOverlay(this._highlightDescriptor.overlay); 1293 delete this._highlightDescriptor; 1294 } 1295 }, 1296 1297 /** 1298 * @param {RegExp} regex 1299 * @param {WebInspector.TextRange} range 1300 * @param {CodeMirror.StringStream} stream 1301 */ 1302 _searchHighlighter: function(regex, range, stream) 1303 { 1304 if (stream.column() === 0) 1305 delete this._searchMatchLength; 1306 if (this._searchMatchLength) { 1307 if (this._searchMatchLength > 1) { 1308 for (var i = 0; i < this._searchMatchLength - 2; ++i) 1309 stream.next(); 1310 this._searchMatchLength = 1; 1311 return "search-highlight"; 1312 } else { 1313 stream.next(); 1314 delete this._searchMatchLength; 1315 return "search-highlight search-highlight-end"; 1316 } 1317 } 1318 var match = stream.match(regex, false); 1319 if (match) { 1320 stream.next(); 1321 var matchLength = match[0].length; 1322 if (matchLength === 1) 1323 return "search-highlight search-highlight-full"; 1324 this._searchMatchLength = matchLength; 1325 return "search-highlight search-highlight-start"; 1326 } 1327 1328 while (!stream.match(regex, false) && stream.next()) {}; 1329 }, 1330 1331 /** 1332 * @param {string} token 1333 * @param {CodeMirror.Pos} selectionStart 1334 * @param {CodeMirror.StringStream} stream 1335 */ 1336 _tokenHighlighter: function(token, selectionStart, stream) 1337 { 1338 var tokenFirstChar = token.charAt(0); 1339 if (stream.match(token) && (stream.eol() || !WebInspector.TextUtils.isWordChar(stream.peek()))) 1340 return stream.column() === selectionStart.ch ? "token-highlight column-with-selection" : "token-highlight"; 1341 1342 var eatenChar; 1343 do { 1344 eatenChar = stream.next(); 1345 } while (eatenChar && (WebInspector.TextUtils.isWordChar(eatenChar) || stream.peek() !== tokenFirstChar)); 1346 }, 1347 1348 /** 1349 * @param {function(CodeMirror.StringStream)} highlighter 1350 */ 1351 _setHighlighter: function(highlighter, selectionStart) 1352 { 1353 var overlayMode = { 1354 token: highlighter 1355 }; 1356 this._codeMirror.addOverlay(overlayMode); 1357 this._highlightDescriptor = { 1358 overlay: overlayMode, 1359 selectionStart: selectionStart 1360 }; 1361 } 1362 } 1363 1364 /** 1365 * @constructor 1366 * @param {CodeMirror} codeMirror 1367 */ 1368 WebInspector.CodeMirrorTextEditor.BlockIndentController = function(codeMirror) 1369 { 1370 codeMirror.addKeyMap(this); 1371 } 1372 1373 WebInspector.CodeMirrorTextEditor.BlockIndentController.prototype = { 1374 name: "blockIndentKeymap", 1375 1376 Enter: function(codeMirror) 1377 { 1378 if (codeMirror.somethingSelected()) 1379 return CodeMirror.Pass; 1380 var cursor = codeMirror.getCursor(); 1381 if (cursor.ch === 0) 1382 return CodeMirror.Pass; 1383 var line = codeMirror.getLine(cursor.line); 1384 if (line.substr(cursor.ch - 1, 2) === "{}") { 1385 codeMirror.execCommand("newlineAndIndent"); 1386 codeMirror.setCursor(cursor); 1387 codeMirror.execCommand("newlineAndIndent"); 1388 codeMirror.execCommand("indentMore"); 1389 } else if (line.substr(cursor.ch - 1, 1) === "{") { 1390 codeMirror.execCommand("newlineAndIndent"); 1391 codeMirror.execCommand("indentMore"); 1392 } else 1393 return CodeMirror.Pass; 1394 }, 1395 1396 "'}'": function(codeMirror) 1397 { 1398 var cursor = codeMirror.getCursor(); 1399 var line = codeMirror.getLine(cursor.line); 1400 for(var i = 0 ; i < line.length; ++i) 1401 if (!WebInspector.TextUtils.isSpaceChar(line.charAt(i))) 1402 return CodeMirror.Pass; 1403 1404 codeMirror.replaceRange("}", cursor); 1405 var matchingBracket = codeMirror.findMatchingBracket(); 1406 if (!matchingBracket || !matchingBracket.match) 1407 return; 1408 1409 line = codeMirror.getLine(matchingBracket.to.line); 1410 var desiredIndentation = 0; 1411 while (desiredIndentation < line.length && WebInspector.TextUtils.isSpaceChar(line.charAt(desiredIndentation))) 1412 ++desiredIndentation; 1413 1414 codeMirror.replaceRange(line.substr(0, desiredIndentation) + "}", new CodeMirror.Pos(cursor.line, 0), new CodeMirror.Pos(cursor.line, cursor.ch + 1)); 1415 } 1416 } 1417 1418 /** 1419 * @constructor 1420 * @param {CodeMirror} codeMirror 1421 */ 1422 WebInspector.CodeMirrorTextEditor.FixWordMovement = function(codeMirror) 1423 { 1424 function moveLeft(shift, codeMirror) 1425 { 1426 var cursor = codeMirror.getCursor("head"); 1427 if (cursor.ch !== 0 || cursor.line === 0) 1428 return CodeMirror.Pass; 1429 codeMirror.setExtending(shift); 1430 codeMirror.execCommand("goLineUp"); 1431 codeMirror.execCommand("goLineEnd") 1432 codeMirror.setExtending(false); 1433 } 1434 function moveRight(shift, codeMirror) 1435 { 1436 var cursor = codeMirror.getCursor("head"); 1437 var line = codeMirror.getLine(cursor.line); 1438 if (cursor.ch !== line.length || cursor.line + 1 === codeMirror.lineCount()) 1439 return CodeMirror.Pass; 1440 codeMirror.setExtending(shift); 1441 codeMirror.execCommand("goLineDown"); 1442 codeMirror.execCommand("goLineStart"); 1443 codeMirror.setExtending(false); 1444 } 1445 function delWordBack(codeMirror) 1446 { 1447 if (codeMirror.somethingSelected()) 1448 return CodeMirror.Pass; 1449 var cursor = codeMirror.getCursor("head"); 1450 if (cursor.ch === 0) 1451 codeMirror.execCommand("delCharBefore"); 1452 else 1453 return CodeMirror.Pass; 1454 } 1455 1456 var modifierKey = WebInspector.isMac() ? "Alt" : "Ctrl"; 1457 var leftKey = modifierKey + "-Left"; 1458 var rightKey = modifierKey + "-Right"; 1459 var keyMap = {}; 1460 keyMap[leftKey] = moveLeft.bind(this, false); 1461 keyMap[rightKey] = moveRight.bind(this, false); 1462 keyMap["Shift-" + leftKey] = moveLeft.bind(this, true); 1463 keyMap["Shift-" + rightKey] = moveRight.bind(this, true); 1464 keyMap[modifierKey + "-Backspace"] = delWordBack.bind(this); 1465 codeMirror.addKeyMap(keyMap); 1466 } 1467 1468 /** 1469 * @constructor 1470 * @implements {WebInspector.SuggestBoxDelegate} 1471 * @param {WebInspector.CodeMirrorTextEditor} textEditor 1472 * @param {CodeMirror} codeMirror 1473 */ 1474 WebInspector.CodeMirrorTextEditor.AutocompleteController = function(textEditor, codeMirror) 1475 { 1476 this._textEditor = textEditor; 1477 this._codeMirror = codeMirror; 1478 this._codeMirror.on("scroll", this._onScroll.bind(this)); 1479 this._codeMirror.on("cursorActivity", this._onCursorActivity.bind(this)); 1480 } 1481 1482 WebInspector.CodeMirrorTextEditor.AutocompleteController.prototype = { 1483 autocomplete: function() 1484 { 1485 var dictionary = this._textEditor._dictionary; 1486 if (!dictionary || this._codeMirror.somethingSelected()) { 1487 this.finishAutocomplete(); 1488 return; 1489 } 1490 1491 var cursor = this._codeMirror.getCursor(); 1492 var substituteRange = this._textEditor._wordRangeForCursorPosition(cursor.line, cursor.ch, false); 1493 if (!substituteRange || substituteRange.startColumn === cursor.ch) { 1494 this.finishAutocomplete(); 1495 return; 1496 } 1497 var prefixRange = substituteRange.clone(); 1498 prefixRange.endColumn = cursor.ch; 1499 1500 var substituteWord = this._textEditor.copyRange(substituteRange); 1501 var hasPrefixInDictionary = dictionary.hasWord(substituteWord); 1502 if (hasPrefixInDictionary) 1503 dictionary.removeWord(substituteWord); 1504 var wordsWithPrefix = dictionary.wordsWithPrefix(this._textEditor.copyRange(prefixRange)); 1505 if (hasPrefixInDictionary) 1506 dictionary.addWord(substituteWord); 1507 1508 function sortSuggestions(a, b) 1509 { 1510 return dictionary.wordCount(b) - dictionary.wordCount(a) || a.length - b.length; 1511 } 1512 1513 wordsWithPrefix.sort(sortSuggestions); 1514 1515 if (!this._suggestBox) { 1516 this._suggestBox = new WebInspector.SuggestBox(this, this._textEditor.element, "generic-suggest", 6); 1517 this._anchorBox = this._anchorBoxForPosition(cursor.line, cursor.ch); 1518 } 1519 this._suggestBox.updateSuggestions(this._anchorBox, wordsWithPrefix, 0, true, this._textEditor.copyRange(prefixRange)); 1520 this._prefixRange = prefixRange; 1521 if (!this._suggestBox.visible()) 1522 this.finishAutocomplete(); 1523 }, 1524 1525 finishAutocomplete: function() 1526 { 1527 if (!this._suggestBox) 1528 return; 1529 this._suggestBox.hide(); 1530 this._suggestBox = null; 1531 this._prefixRange = null; 1532 this._anchorBox = null; 1533 }, 1534 1535 /** 1536 * @param {Event} e 1537 */ 1538 keyDown: function(e) 1539 { 1540 if (!this._suggestBox) 1541 return false; 1542 if (e.keyCode === WebInspector.KeyboardShortcut.Keys.Esc.code) { 1543 this.finishAutocomplete(); 1544 return true; 1545 } 1546 if (e.keyCode === WebInspector.KeyboardShortcut.Keys.Tab.code) { 1547 this._suggestBox.acceptSuggestion(); 1548 this.finishAutocomplete(); 1549 return true; 1550 } 1551 return this._suggestBox.keyPressed(e); 1552 }, 1553 1554 /** 1555 * @param {string} suggestion 1556 * @param {boolean=} isIntermediateSuggestion 1557 */ 1558 applySuggestion: function(suggestion, isIntermediateSuggestion) 1559 { 1560 this._currentSuggestion = suggestion; 1561 }, 1562 1563 acceptSuggestion: function() 1564 { 1565 if (this._prefixRange.endColumn - this._prefixRange.startColumn !== this._currentSuggestion.length) { 1566 var pos = this._textEditor._toPos(this._prefixRange); 1567 this._codeMirror.replaceRange(this._currentSuggestion, pos.start, pos.end, "+autocomplete"); 1568 } 1569 }, 1570 1571 _onScroll: function() 1572 { 1573 if (!this._suggestBox) 1574 return; 1575 var cursor = this._codeMirror.getCursor(); 1576 var scrollInfo = this._codeMirror.getScrollInfo(); 1577 var topmostLineNumber = this._codeMirror.lineAtHeight(scrollInfo.top, "local"); 1578 var bottomLine = this._codeMirror.lineAtHeight(scrollInfo.top + scrollInfo.clientHeight, "local"); 1579 if (cursor.line < topmostLineNumber || cursor.line > bottomLine) 1580 this.finishAutocomplete(); 1581 else { 1582 this._anchorBox = this._anchorBoxForPosition(cursor.line, cursor.ch); 1583 this._suggestBox.setPosition(this._anchorBox); 1584 } 1585 }, 1586 1587 _onCursorActivity: function() 1588 { 1589 if (!this._suggestBox) 1590 return; 1591 var cursor = this._codeMirror.getCursor(); 1592 if (cursor.line !== this._prefixRange.startLine || cursor.ch > this._prefixRange.endColumn || cursor.ch < this._prefixRange.startColumn) 1593 this.finishAutocomplete(); 1594 }, 1595 1596 /** 1597 * @param {number} line 1598 * @param {number} column 1599 * @return {AnchorBox} 1600 */ 1601 _anchorBoxForPosition: function(line, column) 1602 { 1603 var metrics = this._textEditor.cursorPositionToCoordinates(line, column); 1604 return metrics ? new AnchorBox(metrics.x, metrics.y, 0, metrics.height) : null; 1605 }, 1606 } 1607