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 /** 32 * @constructor 33 * @extends {WebInspector.VBox} 34 * @implements {WebInspector.TextEditor} 35 * @param {?string} url 36 * @param {!WebInspector.TextEditorDelegate} delegate 37 */ 38 WebInspector.CodeMirrorTextEditor = function(url, delegate) 39 { 40 WebInspector.VBox.call(this); 41 this._delegate = delegate; 42 this._url = url; 43 44 this.registerRequiredCSS("cm/codemirror.css"); 45 this.registerRequiredCSS("cmdevtools.css"); 46 47 this._codeMirror = new window.CodeMirror(this.element, { 48 lineNumbers: true, 49 gutters: ["CodeMirror-linenumbers"], 50 matchBrackets: true, 51 smartIndent: false, 52 styleSelectedText: true, 53 electricChars: false 54 }); 55 this._codeMirror._codeMirrorTextEditor = this; 56 57 CodeMirror.keyMap["devtools-common"] = { 58 "Left": "goCharLeft", 59 "Right": "goCharRight", 60 "Up": "goLineUp", 61 "Down": "goLineDown", 62 "End": "goLineEnd", 63 "Home": "goLineStartSmart", 64 "PageUp": "goPageUp", 65 "PageDown": "goPageDown", 66 "Delete": "delCharAfter", 67 "Backspace": "delCharBefore", 68 "Tab": "defaultTab", 69 "Shift-Tab": "indentLess", 70 "Enter": "smartNewlineAndIndent", 71 "Ctrl-Space": "autocomplete", 72 "Esc": "dismissMultipleSelections", 73 "Ctrl-M": "gotoMatchingBracket" 74 }; 75 76 CodeMirror.keyMap["devtools-pc"] = { 77 "Ctrl-A": "selectAll", 78 "Ctrl-Z": "undoAndReveal", 79 "Shift-Ctrl-Z": "redoAndReveal", 80 "Ctrl-Y": "redo", 81 "Ctrl-Home": "goDocStart", 82 "Ctrl-Up": "goDocStart", 83 "Ctrl-End": "goDocEnd", 84 "Ctrl-Down": "goDocEnd", 85 "Ctrl-Left": "goGroupLeft", 86 "Ctrl-Right": "goGroupRight", 87 "Alt-Left": "moveCamelLeft", 88 "Alt-Right": "moveCamelRight", 89 "Shift-Alt-Left": "selectCamelLeft", 90 "Shift-Alt-Right": "selectCamelRight", 91 "Ctrl-Backspace": "delGroupBefore", 92 "Ctrl-Delete": "delGroupAfter", 93 "Ctrl-/": "toggleComment", 94 "Ctrl-D": "selectNextOccurrence", 95 "Ctrl-U": "undoLastSelection", 96 fallthrough: "devtools-common" 97 }; 98 99 CodeMirror.keyMap["devtools-mac"] = { 100 "Cmd-A" : "selectAll", 101 "Cmd-Z" : "undoAndReveal", 102 "Shift-Cmd-Z": "redoAndReveal", 103 "Cmd-Up": "goDocStart", 104 "Cmd-Down": "goDocEnd", 105 "Alt-Left": "goGroupLeft", 106 "Alt-Right": "goGroupRight", 107 "Ctrl-Left": "moveCamelLeft", 108 "Ctrl-Right": "moveCamelRight", 109 "Shift-Ctrl-Left": "selectCamelLeft", 110 "Shift-Ctrl-Right": "selectCamelRight", 111 "Cmd-Left": "goLineStartSmart", 112 "Cmd-Right": "goLineEnd", 113 "Alt-Backspace": "delGroupBefore", 114 "Alt-Delete": "delGroupAfter", 115 "Cmd-/": "toggleComment", 116 "Cmd-D": "selectNextOccurrence", 117 "Cmd-U": "undoLastSelection", 118 fallthrough: "devtools-common" 119 }; 120 121 WebInspector.settings.textEditorIndent.addChangeListener(this._updateEditorIndentation, this); 122 WebInspector.settings.textEditorAutoDetectIndent.addChangeListener(this._updateEditorIndentation, this); 123 this._updateEditorIndentation(); 124 WebInspector.settings.showWhitespacesInEditor.addChangeListener(this._updateCodeMirrorMode, this); 125 WebInspector.settings.textEditorBracketMatching.addChangeListener(this._enableBracketMatchingIfNeeded, this); 126 this._enableBracketMatchingIfNeeded(); 127 128 this._codeMirror.setOption("keyMap", WebInspector.isMac() ? "devtools-mac" : "devtools-pc"); 129 130 CodeMirror.commands.maybeAvoidSmartSingleQuotes = this._maybeAvoidSmartQuotes.bind(this, "'"); 131 CodeMirror.commands.maybeAvoidSmartDoubleQuotes = this._maybeAvoidSmartQuotes.bind(this, "\""); 132 this._codeMirror.addKeyMap({ 133 "'": "maybeAvoidSmartSingleQuotes", 134 "'\"'": "maybeAvoidSmartDoubleQuotes" 135 }); 136 137 this._codeMirror.setOption("flattenSpans", false); 138 139 this._codeMirror.setOption("maxHighlightLength", WebInspector.CodeMirrorTextEditor.maxHighlightLength); 140 this._codeMirror.setOption("mode", null); 141 this._codeMirror.setOption("crudeMeasuringFrom", 1000); 142 143 this._shouldClearHistory = true; 144 this._lineSeparator = "\n"; 145 146 this._autocompleteController = WebInspector.CodeMirrorTextEditor.AutocompleteController.Dummy; 147 this._tokenHighlighter = new WebInspector.CodeMirrorTextEditor.TokenHighlighter(this, this._codeMirror); 148 this._blockIndentController = new WebInspector.CodeMirrorTextEditor.BlockIndentController(this._codeMirror); 149 this._fixWordMovement = new WebInspector.CodeMirrorTextEditor.FixWordMovement(this._codeMirror); 150 this._selectNextOccurrenceController = new WebInspector.CodeMirrorTextEditor.SelectNextOccurrenceController(this, this._codeMirror); 151 152 this._codeMirror.on("changes", this._changes.bind(this)); 153 this._codeMirror.on("gutterClick", this._gutterClick.bind(this)); 154 this._codeMirror.on("cursorActivity", this._cursorActivity.bind(this)); 155 this._codeMirror.on("beforeSelectionChange", this._beforeSelectionChange.bind(this)); 156 this._codeMirror.on("scroll", this._scroll.bind(this)); 157 this._codeMirror.on("focus", this._focus.bind(this)); 158 this._codeMirror.on("keyHandled", this._onKeyHandled.bind(this)); 159 this.element.addEventListener("contextmenu", this._contextMenu.bind(this), false); 160 /** 161 * @this {WebInspector.CodeMirrorTextEditor} 162 */ 163 function updateAnticipateJumpFlag(value) 164 { 165 this._isHandlingMouseDownEvent = value; 166 } 167 this.element.addEventListener("mousedown", updateAnticipateJumpFlag.bind(this, true), true); 168 this.element.addEventListener("mousedown", updateAnticipateJumpFlag.bind(this, false), false); 169 170 this.element.style.overflow = "hidden"; 171 this.element.firstChild.classList.add("source-code"); 172 this.element.firstChild.classList.add("fill"); 173 this._elementToWidget = new Map(); 174 this._nestedUpdatesCounter = 0; 175 176 this.element.addEventListener("focus", this._handleElementFocus.bind(this), false); 177 this.element.addEventListener("keydown", this._handleKeyDown.bind(this), true); 178 this.element.addEventListener("keydown", this._handlePostKeyDown.bind(this), false); 179 this.element.tabIndex = 0; 180 181 this._setupWhitespaceHighlight(); 182 } 183 184 /** @typedef {{canceled: boolean, from: !CodeMirror.Pos, to: !CodeMirror.Pos, text: string, origin: string, cancel: function()}} */ 185 WebInspector.CodeMirrorTextEditor.BeforeChangeObject; 186 187 /** @typedef {{from: !CodeMirror.Pos, to: !CodeMirror.Pos, origin: string, text: !Array.<string>, removed: !Array.<string>}} */ 188 WebInspector.CodeMirrorTextEditor.ChangeObject; 189 190 WebInspector.CodeMirrorTextEditor.maxHighlightLength = 1000; 191 192 /** 193 * @param {!CodeMirror} codeMirror 194 */ 195 WebInspector.CodeMirrorTextEditor.autocompleteCommand = function(codeMirror) 196 { 197 codeMirror._codeMirrorTextEditor._autocompleteController.autocomplete(); 198 } 199 CodeMirror.commands.autocomplete = WebInspector.CodeMirrorTextEditor.autocompleteCommand; 200 201 /** 202 * @param {!CodeMirror} codeMirror 203 */ 204 WebInspector.CodeMirrorTextEditor.undoLastSelectionCommand = function(codeMirror) 205 { 206 codeMirror._codeMirrorTextEditor._selectNextOccurrenceController.undoLastSelection(); 207 } 208 CodeMirror.commands.undoLastSelection = WebInspector.CodeMirrorTextEditor.undoLastSelectionCommand; 209 210 /** 211 * @param {!CodeMirror} codeMirror 212 */ 213 WebInspector.CodeMirrorTextEditor.selectNextOccurrenceCommand = function(codeMirror) 214 { 215 codeMirror._codeMirrorTextEditor._selectNextOccurrenceController.selectNextOccurrence(); 216 } 217 CodeMirror.commands.selectNextOccurrence = WebInspector.CodeMirrorTextEditor.selectNextOccurrenceCommand; 218 219 /** 220 * @param {boolean} shift 221 * @param {!CodeMirror} codeMirror 222 */ 223 WebInspector.CodeMirrorTextEditor.moveCamelLeftCommand = function(shift, codeMirror) 224 { 225 codeMirror._codeMirrorTextEditor._doCamelCaseMovement(-1, shift); 226 } 227 CodeMirror.commands.moveCamelLeft = WebInspector.CodeMirrorTextEditor.moveCamelLeftCommand.bind(null, false); 228 CodeMirror.commands.selectCamelLeft = WebInspector.CodeMirrorTextEditor.moveCamelLeftCommand.bind(null, true); 229 230 /** 231 * @param {boolean} shift 232 * @param {!CodeMirror} codeMirror 233 */ 234 WebInspector.CodeMirrorTextEditor.moveCamelRightCommand = function(shift, codeMirror) 235 { 236 codeMirror._codeMirrorTextEditor._doCamelCaseMovement(1, shift); 237 } 238 CodeMirror.commands.moveCamelRight = WebInspector.CodeMirrorTextEditor.moveCamelRightCommand.bind(null, false); 239 CodeMirror.commands.selectCamelRight = WebInspector.CodeMirrorTextEditor.moveCamelRightCommand.bind(null, true); 240 241 /** 242 * @param {!CodeMirror} codeMirror 243 */ 244 CodeMirror.commands.smartNewlineAndIndent = function(codeMirror) 245 { 246 codeMirror.operation(innerSmartNewlineAndIndent.bind(null, codeMirror)); 247 248 function innerSmartNewlineAndIndent(codeMirror) 249 { 250 var selections = codeMirror.listSelections(); 251 var replacements = []; 252 for (var i = 0; i < selections.length; ++i) { 253 var selection = selections[i]; 254 var cur = CodeMirror.cmpPos(selection.head, selection.anchor) < 0 ? selection.head : selection.anchor; 255 var line = codeMirror.getLine(cur.line); 256 var indent = WebInspector.TextUtils.lineIndent(line); 257 replacements.push("\n" + indent.substring(0, Math.min(cur.ch, indent.length))); 258 } 259 codeMirror.replaceSelections(replacements); 260 codeMirror._codeMirrorTextEditor._onAutoAppendedSpaces(); 261 } 262 } 263 264 /** 265 * @param {!CodeMirror} codeMirror 266 */ 267 CodeMirror.commands.gotoMatchingBracket = function(codeMirror) 268 { 269 var updatedSelections = []; 270 var selections = codeMirror.listSelections(); 271 for (var i = 0; i < selections.length; ++i) { 272 var selection = selections[i]; 273 var cursor = selection.head; 274 var matchingBracket = codeMirror.findMatchingBracket(cursor, false, { maxScanLines: 10000 }); 275 var updatedHead = cursor; 276 if (matchingBracket && matchingBracket.match) { 277 var columnCorrection = CodeMirror.cmpPos(matchingBracket.from, cursor) === 0 ? 1 : 0; 278 updatedHead = new CodeMirror.Pos(matchingBracket.to.line, matchingBracket.to.ch + columnCorrection); 279 } 280 updatedSelections.push({ 281 anchor: updatedHead, 282 head: updatedHead 283 }); 284 } 285 codeMirror.setSelections(updatedSelections); 286 } 287 288 /** 289 * @param {!CodeMirror} codemirror 290 */ 291 CodeMirror.commands.undoAndReveal = function(codemirror) 292 { 293 var scrollInfo = codemirror.getScrollInfo(); 294 codemirror.execCommand("undo"); 295 var cursor = codemirror.getCursor("start"); 296 codemirror._codeMirrorTextEditor._innerRevealLine(cursor.line, scrollInfo); 297 codemirror._codeMirrorTextEditor._autocompleteController.finishAutocomplete(); 298 } 299 300 /** 301 * @param {!CodeMirror} codemirror 302 */ 303 CodeMirror.commands.redoAndReveal = function(codemirror) 304 { 305 var scrollInfo = codemirror.getScrollInfo(); 306 codemirror.execCommand("redo"); 307 var cursor = codemirror.getCursor("start"); 308 codemirror._codeMirrorTextEditor._innerRevealLine(cursor.line, scrollInfo); 309 codemirror._codeMirrorTextEditor._autocompleteController.finishAutocomplete(); 310 } 311 312 /** 313 * @return {!Object|undefined} 314 */ 315 CodeMirror.commands.dismissMultipleSelections = function(codemirror) 316 { 317 var selections = codemirror.listSelections(); 318 var selection = selections[0]; 319 if (selections.length === 1) { 320 if (codemirror._codeMirrorTextEditor._isSearchActive()) 321 return CodeMirror.Pass; 322 if (WebInspector.CodeMirrorUtils.toRange(selection.anchor, selection.head).isEmpty()) 323 return CodeMirror.Pass; 324 codemirror.setSelection(selection.anchor, selection.anchor, {scroll: false}); 325 codemirror._codeMirrorTextEditor._revealLine(selection.anchor.line); 326 return; 327 } 328 329 codemirror.setSelection(selection.anchor, selection.head, {scroll: false}); 330 codemirror._codeMirrorTextEditor._revealLine(selection.anchor.line); 331 } 332 333 WebInspector.CodeMirrorTextEditor.LongLineModeLineLengthThreshold = 2000; 334 WebInspector.CodeMirrorTextEditor.MaximumNumberOfWhitespacesPerSingleSpan = 16; 335 WebInspector.CodeMirrorTextEditor.MaxEditableTextSize = 1024 * 1024 * 10; 336 337 WebInspector.CodeMirrorTextEditor.prototype = { 338 /** 339 * @param {string} quoteCharacter 340 * @return {*} 341 */ 342 _maybeAvoidSmartQuotes: function(quoteCharacter) 343 { 344 if (!WebInspector.settings.textEditorBracketMatching.get()) 345 return CodeMirror.Pass; 346 var selections = this.selections(); 347 if (selections.length !== 1 || !selections[0].isEmpty()) 348 return CodeMirror.Pass; 349 350 var selection = selections[0]; 351 var token = this.tokenAtTextPosition(selection.startLine, selection.startColumn); 352 if (!token || token.type.indexOf("string") === -1) 353 return CodeMirror.Pass; 354 var line = this.line(selection.startLine); 355 var tokenValue = line.substring(token.startColumn, token.endColumn); 356 if (tokenValue[0] === tokenValue[tokenValue.length - 1] && (tokenValue[0] === "'" || tokenValue[0] === "\"")) 357 return CodeMirror.Pass; 358 this._codeMirror.replaceSelection(quoteCharacter); 359 }, 360 361 _onKeyHandled: function() 362 { 363 WebInspector.shortcutRegistry.dismissPendingShortcutAction(); 364 }, 365 366 _onAutoAppendedSpaces: function() 367 { 368 this._autoAppendedSpaces = this._autoAppendedSpaces || []; 369 for (var i = 0; i < this._autoAppendedSpaces.length; ++i) { 370 var position = this._autoAppendedSpaces[i].resolve(); 371 if (!position) 372 continue; 373 var line = this.line(position.lineNumber); 374 if (line.length === position.columnNumber && WebInspector.TextUtils.lineIndent(line).length === line.length) 375 this._codeMirror.replaceRange("", new CodeMirror.Pos(position.lineNumber, 0), new CodeMirror.Pos(position.lineNumber, position.columnNumber)); 376 } 377 this._autoAppendedSpaces = []; 378 var selections = this.selections(); 379 for (var i = 0; i < selections.length; ++i) { 380 var selection = selections[i]; 381 this._autoAppendedSpaces.push(this.textEditorPositionHandle(selection.startLine, selection.startColumn)); 382 } 383 }, 384 385 /** 386 * @param {number} lineNumber 387 * @param {number} lineLength 388 * @param {number} charNumber 389 * @return {{lineNumber: number, columnNumber: number}} 390 */ 391 _normalizePositionForOverlappingColumn: function(lineNumber, lineLength, charNumber) 392 { 393 var linesCount = this._codeMirror.lineCount(); 394 var columnNumber = charNumber; 395 if (charNumber < 0 && lineNumber > 0) { 396 --lineNumber; 397 columnNumber = this.line(lineNumber).length; 398 } else if (charNumber >= lineLength && lineNumber < linesCount - 1) { 399 ++lineNumber; 400 columnNumber = 0; 401 } else { 402 columnNumber = Number.constrain(charNumber, 0, lineLength); 403 } 404 return { 405 lineNumber: lineNumber, 406 columnNumber: columnNumber 407 }; 408 }, 409 410 /** 411 * @param {number} lineNumber 412 * @param {number} columnNumber 413 * @param {number} direction 414 * @return {{lineNumber: number, columnNumber: number}} 415 */ 416 _camelCaseMoveFromPosition: function(lineNumber, columnNumber, direction) 417 { 418 /** 419 * @param {number} charNumber 420 * @param {number} length 421 * @return {boolean} 422 */ 423 function valid(charNumber, length) 424 { 425 return charNumber >= 0 && charNumber < length; 426 } 427 428 /** 429 * @param {string} text 430 * @param {number} charNumber 431 * @return {boolean} 432 */ 433 function isWordStart(text, charNumber) 434 { 435 var position = charNumber; 436 var nextPosition = charNumber + 1; 437 return valid(position, text.length) && valid(nextPosition, text.length) 438 && WebInspector.TextUtils.isWordChar(text[position]) && WebInspector.TextUtils.isWordChar(text[nextPosition]) 439 && WebInspector.TextUtils.isUpperCase(text[position]) && WebInspector.TextUtils.isLowerCase(text[nextPosition]); 440 } 441 442 /** 443 * @param {string} text 444 * @param {number} charNumber 445 * @return {boolean} 446 */ 447 function isWordEnd(text, charNumber) 448 { 449 var position = charNumber; 450 var prevPosition = charNumber - 1; 451 return valid(position, text.length) && valid(prevPosition, text.length) 452 && WebInspector.TextUtils.isWordChar(text[position]) && WebInspector.TextUtils.isWordChar(text[prevPosition]) 453 && WebInspector.TextUtils.isUpperCase(text[position]) && WebInspector.TextUtils.isLowerCase(text[prevPosition]); 454 } 455 456 /** 457 * @param {number} lineNumber 458 * @param {number} lineLength 459 * @param {number} columnNumber 460 * @return {{lineNumber: number, columnNumber: number}} 461 */ 462 function constrainPosition(lineNumber, lineLength, columnNumber) 463 { 464 return { 465 lineNumber: lineNumber, 466 columnNumber: Number.constrain(columnNumber, 0, lineLength) 467 }; 468 } 469 470 var text = this.line(lineNumber); 471 var length = text.length; 472 473 if ((columnNumber === length && direction === 1) 474 || (columnNumber === 0 && direction === -1)) 475 return this._normalizePositionForOverlappingColumn(lineNumber, length, columnNumber + direction); 476 477 var charNumber = direction === 1 ? columnNumber : columnNumber - 1; 478 479 // Move through initial spaces if any. 480 while (valid(charNumber, length) && WebInspector.TextUtils.isSpaceChar(text[charNumber])) 481 charNumber += direction; 482 if (!valid(charNumber, length)) 483 return constrainPosition(lineNumber, length, charNumber); 484 485 if (WebInspector.TextUtils.isStopChar(text[charNumber])) { 486 while (valid(charNumber, length) && WebInspector.TextUtils.isStopChar(text[charNumber])) 487 charNumber += direction; 488 if (!valid(charNumber, length)) 489 return constrainPosition(lineNumber, length, charNumber); 490 return { 491 lineNumber: lineNumber, 492 columnNumber: direction === -1 ? charNumber + 1 : charNumber 493 }; 494 } 495 496 charNumber += direction; 497 while (valid(charNumber, length) && !isWordStart(text, charNumber) && !isWordEnd(text, charNumber) && WebInspector.TextUtils.isWordChar(text[charNumber])) 498 charNumber += direction; 499 500 if (!valid(charNumber, length)) 501 return constrainPosition(lineNumber, length, charNumber); 502 if (isWordStart(text, charNumber) || isWordEnd(text, charNumber)) { 503 return { 504 lineNumber: lineNumber, 505 columnNumber: charNumber 506 }; 507 } 508 509 return { 510 lineNumber: lineNumber, 511 columnNumber: direction === -1 ? charNumber + 1 : charNumber 512 }; 513 }, 514 515 /** 516 * @param {number} direction 517 * @param {boolean} shift 518 */ 519 _doCamelCaseMovement: function(direction, shift) 520 { 521 var selections = this.selections(); 522 for (var i = 0; i < selections.length; ++i) { 523 var selection = selections[i]; 524 var move = this._camelCaseMoveFromPosition(selection.endLine, selection.endColumn, direction); 525 selection.endLine = move.lineNumber; 526 selection.endColumn = move.columnNumber; 527 if (!shift) 528 selections[i] = selection.collapseToEnd(); 529 } 530 this.setSelections(selections); 531 }, 532 533 dispose: function() 534 { 535 WebInspector.settings.textEditorIndent.removeChangeListener(this._updateEditorIndentation, this); 536 WebInspector.settings.textEditorAutoDetectIndent.removeChangeListener(this._updateEditorIndentation, this); 537 WebInspector.settings.showWhitespacesInEditor.removeChangeListener(this._updateCodeMirrorMode, this); 538 WebInspector.settings.textEditorBracketMatching.removeChangeListener(this._enableBracketMatchingIfNeeded, this); 539 }, 540 541 _enableBracketMatchingIfNeeded: function() 542 { 543 this._codeMirror.setOption("autoCloseBrackets", WebInspector.settings.textEditorBracketMatching.get() ? { explode: false } : false); 544 }, 545 546 wasShown: function() 547 { 548 if (this._wasOnceShown) 549 return; 550 this._wasOnceShown = true; 551 this._codeMirror.refresh(); 552 }, 553 554 _guessIndentationLevel: function() 555 { 556 var tabRegex = /^\t+/; 557 var tabLines = 0; 558 var indents = {}; 559 function processLine(lineHandle) 560 { 561 var text = lineHandle.text; 562 if (text.length === 0 || !WebInspector.TextUtils.isSpaceChar(text[0])) 563 return; 564 if (tabRegex.test(text)) { 565 ++tabLines; 566 return; 567 } 568 var i = 0; 569 while (i < text.length && WebInspector.TextUtils.isSpaceChar(text[i])) 570 ++i; 571 if (i % 2 !== 0) 572 return; 573 indents[i] = 1 + (indents[i] || 0); 574 } 575 this._codeMirror.eachLine(0, 1000, processLine); 576 577 var onePercentFilterThreshold = this.linesCount / 100; 578 if (tabLines && tabLines > onePercentFilterThreshold) 579 return "\t"; 580 var minimumIndent = Infinity; 581 for (var i in indents) { 582 if (indents[i] < onePercentFilterThreshold) 583 continue; 584 var indent = parseInt(i, 10); 585 if (minimumIndent > indent) 586 minimumIndent = indent; 587 } 588 if (minimumIndent === Infinity) 589 return WebInspector.settings.textEditorIndent.get(); 590 return new Array(minimumIndent + 1).join(" "); 591 }, 592 593 _updateEditorIndentation: function() 594 { 595 var extraKeys = {}; 596 var indent = WebInspector.settings.textEditorIndent.get(); 597 if (WebInspector.settings.textEditorAutoDetectIndent.get()) 598 indent = this._guessIndentationLevel(); 599 if (indent === WebInspector.TextUtils.Indent.TabCharacter) { 600 this._codeMirror.setOption("indentWithTabs", true); 601 this._codeMirror.setOption("indentUnit", 4); 602 } else { 603 this._codeMirror.setOption("indentWithTabs", false); 604 this._codeMirror.setOption("indentUnit", indent.length); 605 extraKeys.Tab = function(codeMirror) 606 { 607 if (codeMirror.somethingSelected()) 608 return CodeMirror.Pass; 609 var pos = codeMirror.getCursor("head"); 610 codeMirror.replaceRange(indent.substring(pos.ch % indent.length), codeMirror.getCursor()); 611 } 612 } 613 this._codeMirror.setOption("extraKeys", extraKeys); 614 this._indentationLevel = indent; 615 }, 616 617 /** 618 * @return {string} 619 */ 620 indent: function() 621 { 622 return this._indentationLevel; 623 }, 624 625 /** 626 * @return {boolean} 627 */ 628 _isSearchActive: function() 629 { 630 return !!this._tokenHighlighter.highlightedRegex(); 631 }, 632 633 /** 634 * @param {!RegExp} regex 635 * @param {?WebInspector.TextRange} range 636 */ 637 highlightSearchResults: function(regex, range) 638 { 639 /** 640 * @this {WebInspector.CodeMirrorTextEditor} 641 */ 642 function innerHighlightRegex() 643 { 644 if (range) { 645 this._revealLine(range.startLine); 646 if (range.endColumn > WebInspector.CodeMirrorTextEditor.maxHighlightLength) 647 this.setSelection(range); 648 else 649 this.setSelection(WebInspector.TextRange.createFromLocation(range.startLine, range.startColumn)); 650 } else { 651 // Collapse selection to end on search start so that we jump to next occurrence on the first enter press. 652 this.setSelection(this.selection().collapseToEnd()); 653 } 654 this._tokenHighlighter.highlightSearchResults(regex, range); 655 } 656 if (!this._selectionBeforeSearch) 657 this._selectionBeforeSearch = this.selection(); 658 this._codeMirror.operation(innerHighlightRegex.bind(this)); 659 }, 660 661 cancelSearchResultsHighlight: function() 662 { 663 this._codeMirror.operation(this._tokenHighlighter.highlightSelectedTokens.bind(this._tokenHighlighter)); 664 if (this._selectionBeforeSearch) { 665 this._reportJump(this._selectionBeforeSearch, this.selection()); 666 delete this._selectionBeforeSearch; 667 } 668 }, 669 670 undo: function() 671 { 672 this._codeMirror.undo(); 673 }, 674 675 redo: function() 676 { 677 this._codeMirror.redo(); 678 }, 679 680 _setupWhitespaceHighlight: function() 681 { 682 if (WebInspector.CodeMirrorTextEditor._whitespaceStyleInjected || !WebInspector.settings.showWhitespacesInEditor.get()) 683 return; 684 WebInspector.CodeMirrorTextEditor._whitespaceStyleInjected = true; 685 const classBase = ".show-whitespaces .CodeMirror .cm-whitespace-"; 686 const spaceChar = ""; 687 var spaceChars = ""; 688 var rules = ""; 689 for (var i = 1; i <= WebInspector.CodeMirrorTextEditor.MaximumNumberOfWhitespacesPerSingleSpan; ++i) { 690 spaceChars += spaceChar; 691 var rule = classBase + i + "::before { content: '" + spaceChars + "';}\n"; 692 rules += rule; 693 } 694 var style = document.createElement("style"); 695 style.textContent = rules; 696 document.head.appendChild(style); 697 }, 698 699 _handleKeyDown: function(e) 700 { 701 if (this._autocompleteController.keyDown(e)) 702 e.consume(true); 703 }, 704 705 _handlePostKeyDown: function(e) 706 { 707 if (e.defaultPrevented) 708 e.consume(true); 709 }, 710 711 /** 712 * @param {?WebInspector.CompletionDictionary} dictionary 713 */ 714 setCompletionDictionary: function(dictionary) 715 { 716 this._autocompleteController.dispose(); 717 if (dictionary) 718 this._autocompleteController = new WebInspector.CodeMirrorTextEditor.AutocompleteController(this, this._codeMirror, dictionary); 719 else 720 this._autocompleteController = WebInspector.CodeMirrorTextEditor.AutocompleteController.Dummy; 721 }, 722 723 /** 724 * @param {number} lineNumber 725 * @param {number} column 726 * @return {?{x: number, y: number, height: number}} 727 */ 728 cursorPositionToCoordinates: function(lineNumber, column) 729 { 730 if (lineNumber >= this._codeMirror.lineCount() || lineNumber < 0 || column < 0 || column > this._codeMirror.getLine(lineNumber).length) 731 return null; 732 733 var metrics = this._codeMirror.cursorCoords(new CodeMirror.Pos(lineNumber, column)); 734 735 return { 736 x: metrics.left, 737 y: metrics.top, 738 height: metrics.bottom - metrics.top 739 }; 740 }, 741 742 /** 743 * @param {number} x 744 * @param {number} y 745 * @return {?WebInspector.TextRange} 746 */ 747 coordinatesToCursorPosition: function(x, y) 748 { 749 var element = document.elementFromPoint(x, y); 750 if (!element || !element.isSelfOrDescendant(this._codeMirror.getWrapperElement())) 751 return null; 752 var gutterBox = this._codeMirror.getGutterElement().boxInWindow(); 753 if (x >= gutterBox.x && x <= gutterBox.x + gutterBox.width && 754 y >= gutterBox.y && y <= gutterBox.y + gutterBox.height) 755 return null; 756 var coords = this._codeMirror.coordsChar({left: x, top: y}); 757 return WebInspector.CodeMirrorUtils.toRange(coords, coords); 758 }, 759 760 /** 761 * @param {number} lineNumber 762 * @param {number} column 763 * @return {?{startColumn: number, endColumn: number, type: string}} 764 */ 765 tokenAtTextPosition: function(lineNumber, column) 766 { 767 if (lineNumber < 0 || lineNumber >= this._codeMirror.lineCount()) 768 return null; 769 var token = this._codeMirror.getTokenAt(new CodeMirror.Pos(lineNumber, (column || 0) + 1)); 770 if (!token || !token.type) 771 return null; 772 return { 773 startColumn: token.start, 774 endColumn: token.end, 775 type: token.type 776 }; 777 }, 778 779 /** 780 * @param {!WebInspector.TextRange} textRange 781 * @return {string} 782 */ 783 copyRange: function(textRange) 784 { 785 var pos = WebInspector.CodeMirrorUtils.toPos(textRange.normalize()); 786 return this._codeMirror.getRange(pos.start, pos.end); 787 }, 788 789 /** 790 * @return {boolean} 791 */ 792 isClean: function() 793 { 794 return this._codeMirror.isClean(); 795 }, 796 797 markClean: function() 798 { 799 this._codeMirror.markClean(); 800 }, 801 802 _hasLongLines: function() 803 { 804 function lineIterator(lineHandle) 805 { 806 if (lineHandle.text.length > WebInspector.CodeMirrorTextEditor.LongLineModeLineLengthThreshold) 807 hasLongLines = true; 808 return hasLongLines; 809 } 810 var hasLongLines = false; 811 this._codeMirror.eachLine(lineIterator); 812 return hasLongLines; 813 }, 814 815 /** 816 * @param {string} mimeType 817 * @return {string} 818 */ 819 _whitespaceOverlayMode: function(mimeType) 820 { 821 var modeName = CodeMirror.mimeModes[mimeType] ? (CodeMirror.mimeModes[mimeType].name || CodeMirror.mimeModes[mimeType]) : CodeMirror.mimeModes["text/plain"]; 822 modeName += "+whitespaces"; 823 if (CodeMirror.modes[modeName]) 824 return modeName; 825 826 function modeConstructor(config, parserConfig) 827 { 828 function nextToken(stream) 829 { 830 if (stream.peek() === " ") { 831 var spaces = 0; 832 while (spaces < WebInspector.CodeMirrorTextEditor.MaximumNumberOfWhitespacesPerSingleSpan && stream.peek() === " ") { 833 ++spaces; 834 stream.next(); 835 } 836 return "whitespace whitespace-" + spaces; 837 } 838 while (!stream.eol() && stream.peek() !== " ") 839 stream.next(); 840 return null; 841 } 842 var whitespaceMode = { 843 token: nextToken 844 }; 845 return CodeMirror.overlayMode(CodeMirror.getMode(config, mimeType), whitespaceMode, false); 846 } 847 CodeMirror.defineMode(modeName, modeConstructor); 848 return modeName; 849 }, 850 851 _enableLongLinesMode: function() 852 { 853 this._codeMirror.setOption("styleSelectedText", false); 854 this._longLinesMode = true; 855 }, 856 857 _disableLongLinesMode: function() 858 { 859 this._codeMirror.setOption("styleSelectedText", true); 860 this._longLinesMode = false; 861 }, 862 863 _updateCodeMirrorMode: function() 864 { 865 this._setupWhitespaceHighlight(); 866 var showWhitespaces = WebInspector.settings.showWhitespacesInEditor.get(); 867 this.element.classList.toggle("show-whitespaces", showWhitespaces); 868 this._codeMirror.setOption("mode", showWhitespaces ? this._whitespaceOverlayMode(this._mimeType) : this._mimeType); 869 }, 870 871 /** 872 * @param {string} mimeType 873 */ 874 setMimeType: function(mimeType) 875 { 876 this._mimeType = mimeType; 877 if (this._hasLongLines()) 878 this._enableLongLinesMode(); 879 else 880 this._disableLongLinesMode(); 881 this._updateCodeMirrorMode(); 882 this._autocompleteController.setMimeType(mimeType); 883 }, 884 885 /** 886 * @param {boolean} readOnly 887 */ 888 setReadOnly: function(readOnly) 889 { 890 this.element.classList.toggle("CodeMirror-readonly", readOnly) 891 this._codeMirror.setOption("readOnly", readOnly); 892 }, 893 894 /** 895 * @return {boolean} 896 */ 897 readOnly: function() 898 { 899 return !!this._codeMirror.getOption("readOnly"); 900 }, 901 902 /** 903 * @param {!Object} highlightDescriptor 904 */ 905 removeHighlight: function(highlightDescriptor) 906 { 907 highlightDescriptor.clear(); 908 }, 909 910 /** 911 * @param {!WebInspector.TextRange} range 912 * @param {string} cssClass 913 * @return {!Object} 914 */ 915 highlightRange: function(range, cssClass) 916 { 917 cssClass = "CodeMirror-persist-highlight " + cssClass; 918 var pos = WebInspector.CodeMirrorUtils.toPos(range); 919 ++pos.end.ch; 920 return this._codeMirror.markText(pos.start, pos.end, { 921 className: cssClass, 922 startStyle: cssClass + "-start", 923 endStyle: cssClass + "-end" 924 }); 925 }, 926 927 /** 928 * @return {!Element} 929 */ 930 defaultFocusedElement: function() 931 { 932 return this.element; 933 }, 934 935 focus: function() 936 { 937 this._codeMirror.focus(); 938 }, 939 940 _handleElementFocus: function() 941 { 942 this._codeMirror.focus(); 943 }, 944 945 beginUpdates: function() 946 { 947 ++this._nestedUpdatesCounter; 948 }, 949 950 endUpdates: function() 951 { 952 if (!--this._nestedUpdatesCounter) 953 this._codeMirror.refresh(); 954 }, 955 956 /** 957 * @param {number} lineNumber 958 */ 959 _revealLine: function(lineNumber) 960 { 961 this._innerRevealLine(lineNumber, this._codeMirror.getScrollInfo()); 962 }, 963 964 /** 965 * @param {number} lineNumber 966 * @param {!{left: number, top: number, width: number, height: number, clientWidth: number, clientHeight: number}} scrollInfo 967 */ 968 _innerRevealLine: function(lineNumber, scrollInfo) 969 { 970 var topLine = this._codeMirror.lineAtHeight(scrollInfo.top, "local"); 971 var bottomLine = this._codeMirror.lineAtHeight(scrollInfo.top + scrollInfo.clientHeight, "local"); 972 var linesPerScreen = bottomLine - topLine + 1; 973 if (lineNumber < topLine) { 974 var topLineToReveal = Math.max(lineNumber - (linesPerScreen / 2) + 1, 0) | 0; 975 this._codeMirror.scrollIntoView(new CodeMirror.Pos(topLineToReveal, 0)); 976 } else if (lineNumber > bottomLine) { 977 var bottomLineToReveal = Math.min(lineNumber + (linesPerScreen / 2) - 1, this.linesCount - 1) | 0; 978 this._codeMirror.scrollIntoView(new CodeMirror.Pos(bottomLineToReveal, 0)); 979 } 980 }, 981 982 _gutterClick: function(instance, lineNumber, gutter, event) 983 { 984 this.dispatchEventToListeners(WebInspector.TextEditor.Events.GutterClick, { lineNumber: lineNumber, event: event }); 985 }, 986 987 _contextMenu: function(event) 988 { 989 var contextMenu = new WebInspector.ContextMenu(event); 990 var target = event.target.enclosingNodeOrSelfWithClass("CodeMirror-gutter-elt"); 991 if (target) 992 this._delegate.populateLineGutterContextMenu(contextMenu, parseInt(target.textContent, 10) - 1); 993 else 994 this._delegate.populateTextAreaContextMenu(contextMenu, 0); 995 contextMenu.appendApplicableItems(this); 996 contextMenu.show(); 997 }, 998 999 /** 1000 * @param {number} lineNumber 1001 * @param {boolean} disabled 1002 * @param {boolean} conditional 1003 */ 1004 addBreakpoint: function(lineNumber, disabled, conditional) 1005 { 1006 if (lineNumber < 0 || lineNumber >= this._codeMirror.lineCount()) 1007 return; 1008 var className = "cm-breakpoint" + (conditional ? " cm-breakpoint-conditional" : "") + (disabled ? " cm-breakpoint-disabled" : ""); 1009 this._codeMirror.addLineClass(lineNumber, "wrap", className); 1010 }, 1011 1012 /** 1013 * @param {number} lineNumber 1014 */ 1015 removeBreakpoint: function(lineNumber) 1016 { 1017 if (lineNumber < 0 || lineNumber >= this._codeMirror.lineCount()) 1018 return; 1019 var wrapClasses = this._codeMirror.getLineHandle(lineNumber).wrapClass; 1020 if (!wrapClasses) 1021 return; 1022 var classes = wrapClasses.split(" "); 1023 for (var i = 0; i < classes.length; ++i) { 1024 if (classes[i].startsWith("cm-breakpoint")) 1025 this._codeMirror.removeLineClass(lineNumber, "wrap", classes[i]); 1026 } 1027 }, 1028 1029 /** 1030 * @param {number} lineNumber 1031 */ 1032 setExecutionLine: function(lineNumber) 1033 { 1034 this.clearPositionHighlight(); 1035 this._executionLine = this._codeMirror.getLineHandle(lineNumber); 1036 if (!this._executionLine) 1037 return; 1038 this._codeMirror.addLineClass(this._executionLine, "wrap", "cm-execution-line"); 1039 }, 1040 1041 clearExecutionLine: function() 1042 { 1043 this.clearPositionHighlight(); 1044 if (this._executionLine) 1045 this._codeMirror.removeLineClass(this._executionLine, "wrap", "cm-execution-line"); 1046 delete this._executionLine; 1047 }, 1048 1049 /** 1050 * @param {number} lineNumber 1051 * @param {string} className 1052 * @param {boolean} toggled 1053 */ 1054 toggleLineClass: function(lineNumber, className, toggled) 1055 { 1056 var lineHandle = this._codeMirror.getLineHandle(lineNumber); 1057 if (!lineHandle) 1058 return; 1059 if (toggled) 1060 this._codeMirror.addLineClass(lineHandle, "wrap", className); 1061 else 1062 this._codeMirror.removeLineClass(lineHandle, "wrap", className); 1063 }, 1064 1065 /** 1066 * @param {number} lineNumber 1067 * @param {!Element} element 1068 */ 1069 addDecoration: function(lineNumber, element) 1070 { 1071 var widget = this._codeMirror.addLineWidget(lineNumber, element); 1072 this._elementToWidget.set(element, widget); 1073 }, 1074 1075 /** 1076 * @param {number} lineNumber 1077 * @param {!Element} element 1078 */ 1079 removeDecoration: function(lineNumber, element) 1080 { 1081 var widget = this._elementToWidget.remove(element); 1082 if (widget) 1083 this._codeMirror.removeLineWidget(widget); 1084 }, 1085 1086 /** 1087 * @param {number} lineNumber 0-based 1088 * @param {number=} columnNumber 1089 * @param {boolean=} shouldHighlight 1090 */ 1091 revealPosition: function(lineNumber, columnNumber, shouldHighlight) 1092 { 1093 lineNumber = Number.constrain(lineNumber, 0, this._codeMirror.lineCount() - 1); 1094 if (typeof columnNumber !== "number") 1095 columnNumber = 0; 1096 columnNumber = Number.constrain(columnNumber, 0, this._codeMirror.getLine(lineNumber).length); 1097 1098 this.clearPositionHighlight(); 1099 this._highlightedLine = this._codeMirror.getLineHandle(lineNumber); 1100 if (!this._highlightedLine) 1101 return; 1102 this._revealLine(lineNumber); 1103 if (shouldHighlight) { 1104 this._codeMirror.addLineClass(this._highlightedLine, null, "cm-highlight"); 1105 this._clearHighlightTimeout = setTimeout(this.clearPositionHighlight.bind(this), 2000); 1106 } 1107 this.setSelection(WebInspector.TextRange.createFromLocation(lineNumber, columnNumber)); 1108 }, 1109 1110 clearPositionHighlight: function() 1111 { 1112 if (this._clearHighlightTimeout) 1113 clearTimeout(this._clearHighlightTimeout); 1114 delete this._clearHighlightTimeout; 1115 1116 if (this._highlightedLine) 1117 this._codeMirror.removeLineClass(this._highlightedLine, null, "cm-highlight"); 1118 delete this._highlightedLine; 1119 }, 1120 1121 /** 1122 * @return {!Array.<!Element>} 1123 */ 1124 elementsToRestoreScrollPositionsFor: function() 1125 { 1126 return []; 1127 }, 1128 1129 /** 1130 * @param {!WebInspector.TextEditor} textEditor 1131 */ 1132 inheritScrollPositions: function(textEditor) 1133 { 1134 }, 1135 1136 /** 1137 * @param {number} width 1138 * @param {number} height 1139 */ 1140 _updatePaddingBottom: function(width, height) 1141 { 1142 var scrollInfo = this._codeMirror.getScrollInfo(); 1143 var newPaddingBottom; 1144 var linesElement = this.element.firstElementChild.querySelector(".CodeMirror-lines"); 1145 var lineCount = this._codeMirror.lineCount(); 1146 if (lineCount <= 1) 1147 newPaddingBottom = 0; 1148 else 1149 newPaddingBottom = Math.max(scrollInfo.clientHeight - this._codeMirror.getLineHandle(this._codeMirror.lastLine()).height, 0); 1150 newPaddingBottom += "px"; 1151 linesElement.style.paddingBottom = newPaddingBottom; 1152 this._codeMirror.setSize(width, height); 1153 }, 1154 1155 _resizeEditor: function() 1156 { 1157 var parentElement = this.element.parentElement; 1158 if (!parentElement || !this.isShowing()) 1159 return; 1160 var scrollLeft = this._codeMirror.doc.scrollLeft; 1161 var scrollTop = this._codeMirror.doc.scrollTop; 1162 var width = parentElement.offsetWidth; 1163 var height = parentElement.offsetHeight - this.element.offsetTop; 1164 this._codeMirror.setSize(width, height); 1165 this._updatePaddingBottom(width, height); 1166 this._codeMirror.scrollTo(scrollLeft, scrollTop); 1167 }, 1168 1169 onResize: function() 1170 { 1171 this._autocompleteController.finishAutocomplete(); 1172 this._resizeEditor(); 1173 }, 1174 1175 /** 1176 * @param {!WebInspector.TextRange} range 1177 * @param {string} text 1178 * @return {!WebInspector.TextRange} 1179 */ 1180 editRange: function(range, text) 1181 { 1182 var pos = WebInspector.CodeMirrorUtils.toPos(range); 1183 this._codeMirror.replaceRange(text, pos.start, pos.end); 1184 var newRange = WebInspector.CodeMirrorUtils.toRange(pos.start, this._codeMirror.posFromIndex(this._codeMirror.indexFromPos(pos.start) + text.length)); 1185 this._delegate.onTextChanged(range, newRange); 1186 if (WebInspector.settings.textEditorAutoDetectIndent.get()) 1187 this._updateEditorIndentation(); 1188 return newRange; 1189 }, 1190 1191 /** 1192 * @param {number} lineNumber 1193 * @param {number} column 1194 * @param {function(string):boolean} isWordChar 1195 * @return {!WebInspector.TextRange} 1196 */ 1197 _wordRangeForCursorPosition: function(lineNumber, column, isWordChar) 1198 { 1199 var line = this.line(lineNumber); 1200 var wordStart = column; 1201 if (column !== 0 && isWordChar(line.charAt(column - 1))) { 1202 wordStart = column - 1; 1203 while (wordStart > 0 && isWordChar(line.charAt(wordStart - 1))) 1204 --wordStart; 1205 } 1206 var wordEnd = column; 1207 while (wordEnd < line.length && isWordChar(line.charAt(wordEnd))) 1208 ++wordEnd; 1209 return new WebInspector.TextRange(lineNumber, wordStart, lineNumber, wordEnd); 1210 }, 1211 1212 /** 1213 * @param {!WebInspector.CodeMirrorTextEditor.ChangeObject} changeObject 1214 * @return {{oldRange: !WebInspector.TextRange, newRange: !WebInspector.TextRange}} 1215 */ 1216 _changeObjectToEditOperation: function(changeObject) 1217 { 1218 var oldRange = WebInspector.CodeMirrorUtils.toRange(changeObject.from, changeObject.to); 1219 var newRange = oldRange.clone(); 1220 var linesAdded = changeObject.text.length; 1221 if (linesAdded === 0) { 1222 newRange.endLine = newRange.startLine; 1223 newRange.endColumn = newRange.startColumn; 1224 } else if (linesAdded === 1) { 1225 newRange.endLine = newRange.startLine; 1226 newRange.endColumn = newRange.startColumn + changeObject.text[0].length; 1227 } else { 1228 newRange.endLine = newRange.startLine + linesAdded - 1; 1229 newRange.endColumn = changeObject.text[linesAdded - 1].length; 1230 } 1231 return { 1232 oldRange: oldRange, 1233 newRange: newRange 1234 }; 1235 }, 1236 1237 /** 1238 * @param {!CodeMirror} codeMirror 1239 * @param {!Array.<!WebInspector.CodeMirrorTextEditor.ChangeObject>} changes 1240 */ 1241 _changes: function(codeMirror, changes) 1242 { 1243 if (!changes.length) 1244 return; 1245 // We do not show "scroll beyond end of file" span for one line documents, so we need to check if "document has one line" changed. 1246 var hasOneLine = this._codeMirror.lineCount() === 1; 1247 if (hasOneLine !== this._hasOneLine) 1248 this._resizeEditor(); 1249 this._hasOneLine = hasOneLine; 1250 var widgets = this._elementToWidget.values(); 1251 for (var i = 0; i < widgets.length; ++i) 1252 this._codeMirror.removeLineWidget(widgets[i]); 1253 this._elementToWidget.clear(); 1254 1255 for (var changeIndex = 0; changeIndex < changes.length; ++changeIndex) { 1256 var changeObject = changes[changeIndex]; 1257 1258 var editInfo = this._changeObjectToEditOperation(changeObject); 1259 if (!this._muteTextChangedEvent) 1260 this._delegate.onTextChanged(editInfo.oldRange, editInfo.newRange); 1261 } 1262 }, 1263 1264 _cursorActivity: function() 1265 { 1266 var start = this._codeMirror.getCursor("anchor"); 1267 var end = this._codeMirror.getCursor("head"); 1268 this._delegate.selectionChanged(WebInspector.CodeMirrorUtils.toRange(start, end)); 1269 if (!this._isSearchActive()) 1270 this._codeMirror.operation(this._tokenHighlighter.highlightSelectedTokens.bind(this._tokenHighlighter)); 1271 }, 1272 1273 /** 1274 * @param {!CodeMirror} codeMirror 1275 * @param {{ranges: !Array.<{head: !CodeMirror.Pos, anchor: !CodeMirror.Pos}>}} selection 1276 */ 1277 _beforeSelectionChange: function(codeMirror, selection) 1278 { 1279 this._selectNextOccurrenceController.selectionWillChange(); 1280 if (!this._isHandlingMouseDownEvent) 1281 return; 1282 if (!selection.ranges.length) 1283 return; 1284 var primarySelection = selection.ranges[0]; 1285 this._reportJump(this.selection(), WebInspector.CodeMirrorUtils.toRange(primarySelection.anchor, primarySelection.head)); 1286 }, 1287 1288 /** 1289 * @param {?WebInspector.TextRange} from 1290 * @param {?WebInspector.TextRange} to 1291 */ 1292 _reportJump: function(from, to) 1293 { 1294 if (from && to && from.equal(to)) 1295 return; 1296 this._delegate.onJumpToPosition(from, to); 1297 }, 1298 1299 _scroll: function() 1300 { 1301 if (this._scrollTimer) 1302 clearTimeout(this._scrollTimer); 1303 var topmostLineNumber = this._codeMirror.lineAtHeight(this._codeMirror.getScrollInfo().top, "local"); 1304 this._scrollTimer = setTimeout(this._delegate.scrollChanged.bind(this._delegate, topmostLineNumber), 100); 1305 }, 1306 1307 _focus: function() 1308 { 1309 this._delegate.editorFocused(); 1310 }, 1311 1312 /** 1313 * @param {number} lineNumber 1314 */ 1315 scrollToLine: function(lineNumber) 1316 { 1317 var pos = new CodeMirror.Pos(lineNumber, 0); 1318 var coords = this._codeMirror.charCoords(pos, "local"); 1319 this._codeMirror.scrollTo(0, coords.top); 1320 }, 1321 1322 /** 1323 * @return {number} 1324 */ 1325 firstVisibleLine: function() 1326 { 1327 return this._codeMirror.lineAtHeight(this._codeMirror.getScrollInfo().top, "local"); 1328 }, 1329 1330 /** 1331 * @return {number} 1332 */ 1333 lastVisibleLine: function() 1334 { 1335 var scrollInfo = this._codeMirror.getScrollInfo(); 1336 return this._codeMirror.lineAtHeight(scrollInfo.top + scrollInfo.clientHeight, "local"); 1337 }, 1338 1339 /** 1340 * @return {!WebInspector.TextRange} 1341 */ 1342 selection: function() 1343 { 1344 var start = this._codeMirror.getCursor("anchor"); 1345 var end = this._codeMirror.getCursor("head"); 1346 1347 return WebInspector.CodeMirrorUtils.toRange(start, end); 1348 }, 1349 1350 /** 1351 * @return {!Array.<!WebInspector.TextRange>} 1352 */ 1353 selections: function() 1354 { 1355 var selectionList = this._codeMirror.listSelections(); 1356 var result = []; 1357 for (var i = 0; i < selectionList.length; ++i) { 1358 var selection = selectionList[i]; 1359 result.push(WebInspector.CodeMirrorUtils.toRange(selection.anchor, selection.head)); 1360 } 1361 return result; 1362 }, 1363 1364 /** 1365 * @return {?WebInspector.TextRange} 1366 */ 1367 lastSelection: function() 1368 { 1369 return this._lastSelection; 1370 }, 1371 1372 /** 1373 * @param {!WebInspector.TextRange} textRange 1374 */ 1375 setSelection: function(textRange) 1376 { 1377 this._lastSelection = textRange; 1378 var pos = WebInspector.CodeMirrorUtils.toPos(textRange); 1379 this._codeMirror.setSelection(pos.start, pos.end); 1380 }, 1381 1382 /** 1383 * @param {!Array.<!WebInspector.TextRange>} ranges 1384 * @param {number=} primarySelectionIndex 1385 */ 1386 setSelections: function(ranges, primarySelectionIndex) 1387 { 1388 var selections = []; 1389 for (var i = 0; i < ranges.length; ++i) { 1390 var selection = WebInspector.CodeMirrorUtils.toPos(ranges[i]); 1391 selections.push({ 1392 anchor: selection.start, 1393 head: selection.end 1394 }); 1395 } 1396 primarySelectionIndex = primarySelectionIndex || 0; 1397 this._codeMirror.setSelections(selections, primarySelectionIndex, { scroll: false }); 1398 }, 1399 1400 /** 1401 * @param {string} text 1402 */ 1403 _detectLineSeparator: function(text) 1404 { 1405 this._lineSeparator = text.indexOf("\r\n") >= 0 ? "\r\n" : "\n"; 1406 }, 1407 1408 /** 1409 * @param {string} text 1410 */ 1411 setText: function(text) 1412 { 1413 this._muteTextChangedEvent = true; 1414 if (text.length > WebInspector.CodeMirrorTextEditor.MaxEditableTextSize) { 1415 this._autocompleteController.setEnabled(false); 1416 this.setReadOnly(true); 1417 } 1418 this._codeMirror.setValue(text); 1419 this._updateEditorIndentation(); 1420 if (this._shouldClearHistory) { 1421 this._codeMirror.clearHistory(); 1422 this._shouldClearHistory = false; 1423 } 1424 this._detectLineSeparator(text); 1425 delete this._muteTextChangedEvent; 1426 }, 1427 1428 /** 1429 * @return {string} 1430 */ 1431 text: function() 1432 { 1433 return this._codeMirror.getValue().replace(/\n/g, this._lineSeparator); 1434 }, 1435 1436 /** 1437 * @return {!WebInspector.TextRange} 1438 */ 1439 range: function() 1440 { 1441 var lineCount = this.linesCount; 1442 var lastLine = this._codeMirror.getLine(lineCount - 1); 1443 return WebInspector.CodeMirrorUtils.toRange(new CodeMirror.Pos(0, 0), new CodeMirror.Pos(lineCount - 1, lastLine.length)); 1444 }, 1445 1446 /** 1447 * @param {number} lineNumber 1448 * @return {string} 1449 */ 1450 line: function(lineNumber) 1451 { 1452 return this._codeMirror.getLine(lineNumber); 1453 }, 1454 1455 /** 1456 * @return {number} 1457 */ 1458 get linesCount() 1459 { 1460 return this._codeMirror.lineCount(); 1461 }, 1462 1463 /** 1464 * @param {number} line 1465 * @param {string} name 1466 * @param {?Object} value 1467 */ 1468 setAttribute: function(line, name, value) 1469 { 1470 if (line < 0 || line >= this._codeMirror.lineCount()) 1471 return; 1472 var handle = this._codeMirror.getLineHandle(line); 1473 if (handle.attributes === undefined) handle.attributes = {}; 1474 handle.attributes[name] = value; 1475 }, 1476 1477 /** 1478 * @param {number} line 1479 * @param {string} name 1480 * @return {?Object} value 1481 */ 1482 getAttribute: function(line, name) 1483 { 1484 if (line < 0 || line >= this._codeMirror.lineCount()) 1485 return null; 1486 var handle = this._codeMirror.getLineHandle(line); 1487 return handle.attributes && handle.attributes[name] !== undefined ? handle.attributes[name] : null; 1488 }, 1489 1490 /** 1491 * @param {number} line 1492 * @param {string} name 1493 */ 1494 removeAttribute: function(line, name) 1495 { 1496 if (line < 0 || line >= this._codeMirror.lineCount()) 1497 return; 1498 var handle = this._codeMirror.getLineHandle(line); 1499 if (handle && handle.attributes) 1500 delete handle.attributes[name]; 1501 }, 1502 1503 /** 1504 * @param {number} lineNumber 1505 * @param {number} columnNumber 1506 * @return {!WebInspector.TextEditorPositionHandle} 1507 */ 1508 textEditorPositionHandle: function(lineNumber, columnNumber) 1509 { 1510 return new WebInspector.CodeMirrorPositionHandle(this._codeMirror, new CodeMirror.Pos(lineNumber, columnNumber)); 1511 }, 1512 1513 __proto__: WebInspector.VBox.prototype 1514 } 1515 1516 /** 1517 * @constructor 1518 * @implements {WebInspector.TextEditorPositionHandle} 1519 * @param {!CodeMirror} codeMirror 1520 * @param {!CodeMirror.Pos} pos 1521 */ 1522 WebInspector.CodeMirrorPositionHandle = function(codeMirror, pos) 1523 { 1524 this._codeMirror = codeMirror; 1525 this._lineHandle = codeMirror.getLineHandle(pos.line); 1526 this._columnNumber = pos.ch; 1527 } 1528 1529 WebInspector.CodeMirrorPositionHandle.prototype = { 1530 /** 1531 * @return {?{lineNumber: number, columnNumber: number}} 1532 */ 1533 resolve: function() 1534 { 1535 var lineNumber = this._codeMirror.getLineNumber(this._lineHandle); 1536 if (typeof lineNumber !== "number") 1537 return null; 1538 return { 1539 lineNumber: lineNumber, 1540 columnNumber: this._columnNumber 1541 }; 1542 }, 1543 1544 /** 1545 * @param {!WebInspector.TextEditorPositionHandle} positionHandle 1546 * @return {boolean} 1547 */ 1548 equal: function(positionHandle) 1549 { 1550 return positionHandle._lineHandle === this._lineHandle && positionHandle._columnNumber == this._columnNumber && positionHandle._codeMirror === this._codeMirror; 1551 } 1552 } 1553 1554 /** 1555 * @constructor 1556 * @param {!WebInspector.CodeMirrorTextEditor} textEditor 1557 * @param {!CodeMirror} codeMirror 1558 */ 1559 WebInspector.CodeMirrorTextEditor.TokenHighlighter = function(textEditor, codeMirror) 1560 { 1561 this._textEditor = textEditor; 1562 this._codeMirror = codeMirror; 1563 } 1564 1565 WebInspector.CodeMirrorTextEditor.TokenHighlighter.prototype = { 1566 /** 1567 * @param {!RegExp} regex 1568 * @param {?WebInspector.TextRange} range 1569 */ 1570 highlightSearchResults: function(regex, range) 1571 { 1572 var oldRegex = this._highlightRegex; 1573 this._highlightRegex = regex; 1574 this._highlightRange = range; 1575 if (this._searchResultMarker) { 1576 this._searchResultMarker.clear(); 1577 delete this._searchResultMarker; 1578 } 1579 if (this._highlightDescriptor && this._highlightDescriptor.selectionStart) 1580 this._codeMirror.removeLineClass(this._highlightDescriptor.selectionStart.line, "wrap", "cm-line-with-selection"); 1581 var selectionStart = this._highlightRange ? new CodeMirror.Pos(this._highlightRange.startLine, this._highlightRange.startColumn) : null; 1582 if (selectionStart) 1583 this._codeMirror.addLineClass(selectionStart.line, "wrap", "cm-line-with-selection"); 1584 if (this._highlightRegex === oldRegex) { 1585 // Do not re-add overlay mode if regex did not change for better performance. 1586 if (this._highlightDescriptor) 1587 this._highlightDescriptor.selectionStart = selectionStart; 1588 } else { 1589 this._removeHighlight(); 1590 this._setHighlighter(this._searchHighlighter.bind(this, this._highlightRegex), selectionStart); 1591 } 1592 if (this._highlightRange) { 1593 var pos = WebInspector.CodeMirrorUtils.toPos(this._highlightRange); 1594 this._searchResultMarker = this._codeMirror.markText(pos.start, pos.end, {className: "cm-column-with-selection"}); 1595 } 1596 }, 1597 1598 /** 1599 * @return {!RegExp|undefined} 1600 */ 1601 highlightedRegex: function() 1602 { 1603 return this._highlightRegex; 1604 }, 1605 1606 highlightSelectedTokens: function() 1607 { 1608 delete this._highlightRegex; 1609 delete this._highlightRange; 1610 1611 if (this._highlightDescriptor && this._highlightDescriptor.selectionStart) 1612 this._codeMirror.removeLineClass(this._highlightDescriptor.selectionStart.line, "wrap", "cm-line-with-selection"); 1613 this._removeHighlight(); 1614 var selectionStart = this._codeMirror.getCursor("start"); 1615 var selectionEnd = this._codeMirror.getCursor("end"); 1616 if (selectionStart.line !== selectionEnd.line) 1617 return; 1618 if (selectionStart.ch === selectionEnd.ch) 1619 return; 1620 1621 var selections = this._codeMirror.getSelections(); 1622 if (selections.length > 1) 1623 return; 1624 var selectedText = selections[0]; 1625 if (this._isWord(selectedText, selectionStart.line, selectionStart.ch, selectionEnd.ch)) { 1626 if (selectionStart) 1627 this._codeMirror.addLineClass(selectionStart.line, "wrap", "cm-line-with-selection") 1628 this._setHighlighter(this._tokenHighlighter.bind(this, selectedText, selectionStart), selectionStart); 1629 } 1630 }, 1631 1632 /** 1633 * @param {string} selectedText 1634 * @param {number} lineNumber 1635 * @param {number} startColumn 1636 * @param {number} endColumn 1637 */ 1638 _isWord: function(selectedText, lineNumber, startColumn, endColumn) 1639 { 1640 var line = this._codeMirror.getLine(lineNumber); 1641 var leftBound = startColumn === 0 || !WebInspector.TextUtils.isWordChar(line.charAt(startColumn - 1)); 1642 var rightBound = endColumn === line.length || !WebInspector.TextUtils.isWordChar(line.charAt(endColumn)); 1643 return leftBound && rightBound && WebInspector.TextUtils.isWord(selectedText); 1644 }, 1645 1646 _removeHighlight: function() 1647 { 1648 if (this._highlightDescriptor) { 1649 this._codeMirror.removeOverlay(this._highlightDescriptor.overlay); 1650 delete this._highlightDescriptor; 1651 } 1652 }, 1653 1654 /** 1655 * @param {!RegExp} regex 1656 * @param {!CodeMirror.StringStream} stream 1657 */ 1658 _searchHighlighter: function(regex, stream) 1659 { 1660 if (stream.column() === 0) 1661 delete this._searchMatchLength; 1662 if (this._searchMatchLength) { 1663 if (this._searchMatchLength > 2) { 1664 for (var i = 0; i < this._searchMatchLength - 2; ++i) 1665 stream.next(); 1666 this._searchMatchLength = 1; 1667 return "search-highlight"; 1668 } else { 1669 stream.next(); 1670 delete this._searchMatchLength; 1671 return "search-highlight search-highlight-end"; 1672 } 1673 } 1674 var match = stream.match(regex, false); 1675 if (match) { 1676 stream.next(); 1677 var matchLength = match[0].length; 1678 if (matchLength === 1) 1679 return "search-highlight search-highlight-full"; 1680 this._searchMatchLength = matchLength; 1681 return "search-highlight search-highlight-start"; 1682 } 1683 1684 while (!stream.match(regex, false) && stream.next()) {}; 1685 }, 1686 1687 /** 1688 * @param {string} token 1689 * @param {!CodeMirror.Pos} selectionStart 1690 * @param {!CodeMirror.StringStream} stream 1691 */ 1692 _tokenHighlighter: function(token, selectionStart, stream) 1693 { 1694 var tokenFirstChar = token.charAt(0); 1695 if (stream.match(token) && (stream.eol() || !WebInspector.TextUtils.isWordChar(stream.peek()))) 1696 return stream.column() === selectionStart.ch ? "token-highlight column-with-selection" : "token-highlight"; 1697 1698 var eatenChar; 1699 do { 1700 eatenChar = stream.next(); 1701 } while (eatenChar && (WebInspector.TextUtils.isWordChar(eatenChar) || stream.peek() !== tokenFirstChar)); 1702 }, 1703 1704 /** 1705 * @param {function(!CodeMirror.StringStream)} highlighter 1706 * @param {?CodeMirror.Pos} selectionStart 1707 */ 1708 _setHighlighter: function(highlighter, selectionStart) 1709 { 1710 var overlayMode = { 1711 token: highlighter 1712 }; 1713 this._codeMirror.addOverlay(overlayMode); 1714 this._highlightDescriptor = { 1715 overlay: overlayMode, 1716 selectionStart: selectionStart 1717 }; 1718 } 1719 } 1720 1721 /** 1722 * @constructor 1723 * @param {!CodeMirror} codeMirror 1724 */ 1725 WebInspector.CodeMirrorTextEditor.BlockIndentController = function(codeMirror) 1726 { 1727 codeMirror.addKeyMap(this); 1728 } 1729 1730 WebInspector.CodeMirrorTextEditor.BlockIndentController.prototype = { 1731 name: "blockIndentKeymap", 1732 1733 /** 1734 * @return {*} 1735 */ 1736 Enter: function(codeMirror) 1737 { 1738 var selections = codeMirror.listSelections(); 1739 var replacements = []; 1740 var allSelectionsAreCollapsedBlocks = false; 1741 for (var i = 0; i < selections.length; ++i) { 1742 var selection = selections[i]; 1743 var start = CodeMirror.cmpPos(selection.head, selection.anchor) < 0 ? selection.head : selection.anchor; 1744 var line = codeMirror.getLine(start.line); 1745 var indent = WebInspector.TextUtils.lineIndent(line); 1746 var indentToInsert = "\n" + indent + codeMirror._codeMirrorTextEditor.indent(); 1747 var isCollapsedBlock = false; 1748 if (selection.head.ch === 0) 1749 return CodeMirror.Pass; 1750 if (line.substr(selection.head.ch - 1, 2) === "{}") { 1751 indentToInsert += "\n" + indent; 1752 isCollapsedBlock = true; 1753 } else if (line.substr(selection.head.ch - 1, 1) !== "{") { 1754 return CodeMirror.Pass; 1755 } 1756 if (i > 0 && allSelectionsAreCollapsedBlocks !== isCollapsedBlock) 1757 return CodeMirror.Pass; 1758 replacements.push(indentToInsert); 1759 allSelectionsAreCollapsedBlocks = isCollapsedBlock; 1760 } 1761 codeMirror.replaceSelections(replacements); 1762 if (!allSelectionsAreCollapsedBlocks) { 1763 codeMirror._codeMirrorTextEditor._onAutoAppendedSpaces(); 1764 return; 1765 } 1766 selections = codeMirror.listSelections(); 1767 var updatedSelections = []; 1768 for (var i = 0; i < selections.length; ++i) { 1769 var selection = selections[i]; 1770 var line = codeMirror.getLine(selection.head.line - 1); 1771 var position = new CodeMirror.Pos(selection.head.line - 1, line.length); 1772 updatedSelections.push({ 1773 head: position, 1774 anchor: position 1775 }); 1776 } 1777 codeMirror.setSelections(updatedSelections); 1778 codeMirror._codeMirrorTextEditor._onAutoAppendedSpaces(); 1779 }, 1780 1781 /** 1782 * @return {*} 1783 */ 1784 "'}'": function(codeMirror) 1785 { 1786 if (codeMirror.somethingSelected()) 1787 return CodeMirror.Pass; 1788 1789 var selections = codeMirror.listSelections(); 1790 var replacements = []; 1791 for (var i = 0; i < selections.length; ++i) { 1792 var selection = selections[i]; 1793 var line = codeMirror.getLine(selection.head.line); 1794 if (line !== WebInspector.TextUtils.lineIndent(line)) 1795 return CodeMirror.Pass; 1796 replacements.push("}"); 1797 } 1798 1799 codeMirror.replaceSelections(replacements); 1800 selections = codeMirror.listSelections(); 1801 replacements = []; 1802 var updatedSelections = []; 1803 for (var i = 0; i < selections.length; ++i) { 1804 var selection = selections[i]; 1805 var matchingBracket = codeMirror.findMatchingBracket(selection.head); 1806 if (!matchingBracket || !matchingBracket.match) 1807 return; 1808 updatedSelections.push({ 1809 head: selection.head, 1810 anchor: new CodeMirror.Pos(selection.head.line, 0) 1811 }); 1812 var line = codeMirror.getLine(matchingBracket.to.line); 1813 var indent = WebInspector.TextUtils.lineIndent(line); 1814 replacements.push(indent + "}"); 1815 } 1816 codeMirror.setSelections(updatedSelections); 1817 codeMirror.replaceSelections(replacements); 1818 } 1819 } 1820 1821 /** 1822 * @constructor 1823 * @param {!CodeMirror} codeMirror 1824 */ 1825 WebInspector.CodeMirrorTextEditor.FixWordMovement = function(codeMirror) 1826 { 1827 function moveLeft(shift, codeMirror) 1828 { 1829 codeMirror.setExtending(shift); 1830 var cursor = codeMirror.getCursor("head"); 1831 codeMirror.execCommand("goGroupLeft"); 1832 var newCursor = codeMirror.getCursor("head"); 1833 if (newCursor.ch === 0 && newCursor.line !== 0) { 1834 codeMirror.setExtending(false); 1835 return; 1836 } 1837 1838 var skippedText = codeMirror.getRange(newCursor, cursor, "#"); 1839 if (/^\s+$/.test(skippedText)) 1840 codeMirror.execCommand("goGroupLeft"); 1841 codeMirror.setExtending(false); 1842 } 1843 1844 function moveRight(shift, codeMirror) 1845 { 1846 codeMirror.setExtending(shift); 1847 var cursor = codeMirror.getCursor("head"); 1848 codeMirror.execCommand("goGroupRight"); 1849 var newCursor = codeMirror.getCursor("head"); 1850 if (newCursor.ch === 0 && newCursor.line !== 0) { 1851 codeMirror.setExtending(false); 1852 return; 1853 } 1854 1855 var skippedText = codeMirror.getRange(cursor, newCursor, "#"); 1856 if (/^\s+$/.test(skippedText)) 1857 codeMirror.execCommand("goGroupRight"); 1858 codeMirror.setExtending(false); 1859 } 1860 1861 var modifierKey = WebInspector.isMac() ? "Alt" : "Ctrl"; 1862 var leftKey = modifierKey + "-Left"; 1863 var rightKey = modifierKey + "-Right"; 1864 var keyMap = {}; 1865 keyMap[leftKey] = moveLeft.bind(null, false); 1866 keyMap[rightKey] = moveRight.bind(null, false); 1867 keyMap["Shift-" + leftKey] = moveLeft.bind(null, true); 1868 keyMap["Shift-" + rightKey] = moveRight.bind(null, true); 1869 codeMirror.addKeyMap(keyMap); 1870 } 1871 1872 /** 1873 * @interface 1874 */ 1875 WebInspector.CodeMirrorTextEditor.AutocompleteControllerAPI = function() {} 1876 1877 WebInspector.CodeMirrorTextEditor.AutocompleteControllerAPI.prototype = { 1878 dispose: function() { }, 1879 1880 /** 1881 * @param {boolean} enabled 1882 */ 1883 setEnabled: function(enabled) { }, 1884 1885 /** 1886 * @param {string} mimeType 1887 */ 1888 setMimeType: function(mimeType) { }, 1889 1890 autocomplete: function() { }, 1891 1892 finishAutocomplete: function() { }, 1893 1894 /** 1895 * @param {!Event} e 1896 * @return {boolean} 1897 */ 1898 keyDown: function(e) { } 1899 } 1900 1901 /** 1902 * @constructor 1903 * @implements {WebInspector.CodeMirrorTextEditor.AutocompleteControllerAPI} 1904 */ 1905 WebInspector.CodeMirrorTextEditor.DummyAutocompleteController = function() { } 1906 1907 WebInspector.CodeMirrorTextEditor.DummyAutocompleteController.prototype = { 1908 dispose: function() { }, 1909 1910 /** 1911 * @param {boolean} enabled 1912 */ 1913 setEnabled: function(enabled) { }, 1914 1915 /** 1916 * @param {string} mimeType 1917 */ 1918 setMimeType: function(mimeType) { }, 1919 1920 autocomplete: function() { }, 1921 1922 finishAutocomplete: function() { }, 1923 1924 /** 1925 * @param {!Event} e 1926 * @return {boolean} 1927 */ 1928 keyDown: function(e) 1929 { 1930 return false; 1931 } 1932 } 1933 1934 /** 1935 * @constructor 1936 * @implements {WebInspector.SuggestBoxDelegate} 1937 * @implements {WebInspector.CodeMirrorTextEditor.AutocompleteControllerAPI} 1938 * @param {!WebInspector.CodeMirrorTextEditor} textEditor 1939 * @param {!CodeMirror} codeMirror 1940 * @param {?WebInspector.CompletionDictionary} dictionary 1941 */ 1942 WebInspector.CodeMirrorTextEditor.AutocompleteController = function(textEditor, codeMirror, dictionary) 1943 { 1944 this._textEditor = textEditor; 1945 this._codeMirror = codeMirror; 1946 1947 this._onScroll = this._onScroll.bind(this); 1948 this._onCursorActivity = this._onCursorActivity.bind(this); 1949 this._changes = this._changes.bind(this); 1950 this._beforeChange = this._beforeChange.bind(this); 1951 this._blur = this._blur.bind(this); 1952 this._codeMirror.on("scroll", this._onScroll); 1953 this._codeMirror.on("cursorActivity", this._onCursorActivity); 1954 this._codeMirror.on("changes", this._changes); 1955 this._codeMirror.on("beforeChange", this._beforeChange); 1956 this._codeMirror.on("blur", this._blur); 1957 1958 this._additionalWordChars = WebInspector.CodeMirrorTextEditor._NoAdditionalWordChars; 1959 this._enabled = true; 1960 1961 this._dictionary = dictionary; 1962 this._addTextToCompletionDictionary(this._textEditor.text()); 1963 } 1964 1965 WebInspector.CodeMirrorTextEditor.AutocompleteController.Dummy = new WebInspector.CodeMirrorTextEditor.DummyAutocompleteController(); 1966 WebInspector.CodeMirrorTextEditor._NoAdditionalWordChars = {}; 1967 WebInspector.CodeMirrorTextEditor._CSSAdditionalWordChars = { ".": true, "-": true }; 1968 1969 WebInspector.CodeMirrorTextEditor.AutocompleteController.prototype = { 1970 dispose: function() 1971 { 1972 this._codeMirror.off("scroll", this._onScroll); 1973 this._codeMirror.off("cursorActivity", this._onCursorActivity); 1974 this._codeMirror.off("changes", this._changes); 1975 this._codeMirror.off("beforeChange", this._beforeChange); 1976 this._codeMirror.off("blur", this._blur); 1977 }, 1978 1979 /** 1980 * @param {boolean} enabled 1981 */ 1982 setEnabled: function(enabled) 1983 { 1984 if (enabled === this._enabled) 1985 return; 1986 this._enabled = enabled; 1987 if (!enabled) 1988 this._dictionary.reset(); 1989 else 1990 this._addTextToCompletionDictionary(this._textEditor.text()); 1991 }, 1992 1993 /** 1994 * @param {string} mimeType 1995 */ 1996 setMimeType: function(mimeType) 1997 { 1998 var additionalWordChars = mimeType.indexOf("css") !== -1 ? WebInspector.CodeMirrorTextEditor._CSSAdditionalWordChars : WebInspector.CodeMirrorTextEditor._NoAdditionalWordChars; 1999 if (additionalWordChars !== this._additionalWordChars) { 2000 this._additionalWordChars = additionalWordChars; 2001 this._dictionary.reset(); 2002 this._addTextToCompletionDictionary(this._textEditor.text()); 2003 } 2004 }, 2005 2006 /** 2007 * @param {string} char 2008 * @return {boolean} 2009 */ 2010 _isWordChar: function(char) 2011 { 2012 return WebInspector.TextUtils.isWordChar(char) || !!this._additionalWordChars[char]; 2013 }, 2014 2015 /** 2016 * @param {string} word 2017 * @return {boolean} 2018 */ 2019 _shouldProcessWordForAutocompletion: function(word) 2020 { 2021 return !!word.length && (word[0] < '0' || word[0] > '9'); 2022 }, 2023 2024 /** 2025 * @param {string} text 2026 */ 2027 _addTextToCompletionDictionary: function(text) 2028 { 2029 if (!this._enabled) 2030 return; 2031 var words = WebInspector.TextUtils.textToWords(text, this._isWordChar.bind(this)); 2032 for (var i = 0; i < words.length; ++i) { 2033 if (this._shouldProcessWordForAutocompletion(words[i])) 2034 this._dictionary.addWord(words[i]); 2035 } 2036 }, 2037 2038 /** 2039 * @param {string} text 2040 */ 2041 _removeTextFromCompletionDictionary: function(text) 2042 { 2043 if (!this._enabled) 2044 return; 2045 var words = WebInspector.TextUtils.textToWords(text, this._isWordChar.bind(this)); 2046 for (var i = 0; i < words.length; ++i) { 2047 if (this._shouldProcessWordForAutocompletion(words[i])) 2048 this._dictionary.removeWord(words[i]); 2049 } 2050 }, 2051 2052 /** 2053 * @param {!CodeMirror} codeMirror 2054 * @param {!WebInspector.CodeMirrorTextEditor.BeforeChangeObject} changeObject 2055 */ 2056 _beforeChange: function(codeMirror, changeObject) 2057 { 2058 if (!this._enabled) 2059 return; 2060 this._updatedLines = this._updatedLines || {}; 2061 for (var i = changeObject.from.line; i <= changeObject.to.line; ++i) 2062 this._updatedLines[i] = this._textEditor.line(i); 2063 }, 2064 2065 /** 2066 * @param {!CodeMirror} codeMirror 2067 * @param {!Array.<!WebInspector.CodeMirrorTextEditor.ChangeObject>} changes 2068 */ 2069 _changes: function(codeMirror, changes) 2070 { 2071 if (!changes.length || !this._enabled) 2072 return; 2073 2074 if (this._updatedLines) { 2075 for (var lineNumber in this._updatedLines) 2076 this._removeTextFromCompletionDictionary(this._updatedLines[lineNumber]); 2077 delete this._updatedLines; 2078 } 2079 2080 var linesToUpdate = {}; 2081 var singleCharInput = false; 2082 for (var changeIndex = 0; changeIndex < changes.length; ++changeIndex) { 2083 var changeObject = changes[changeIndex]; 2084 singleCharInput = (changeObject.origin === "+input" && changeObject.text.length === 1 && changeObject.text[0].length === 1) || 2085 (this._suggestBox && changeObject.origin === "+delete" && changeObject.removed.length === 1 && changeObject.removed[0].length === 1); 2086 2087 var editInfo = this._textEditor._changeObjectToEditOperation(changeObject); 2088 for (var i = editInfo.newRange.startLine; i <= editInfo.newRange.endLine; ++i) 2089 linesToUpdate[i] = this._textEditor.line(i); 2090 } 2091 for (var lineNumber in linesToUpdate) 2092 this._addTextToCompletionDictionary(linesToUpdate[lineNumber]); 2093 2094 if (singleCharInput) 2095 this.autocomplete(); 2096 }, 2097 2098 _blur: function() 2099 { 2100 this.finishAutocomplete(); 2101 }, 2102 2103 /** 2104 * @param {number} lineNumber 2105 * @param {number} columnNumber 2106 * @return {!WebInspector.TextRange} 2107 */ 2108 _autocompleteWordRange: function(lineNumber, columnNumber) 2109 { 2110 return this._textEditor._wordRangeForCursorPosition(lineNumber, columnNumber, this._isWordChar.bind(this)); 2111 }, 2112 2113 /** 2114 * @param {!WebInspector.TextRange} mainSelection 2115 * @param {!Array.<!{head: !CodeMirror.Pos, anchor: !CodeMirror.Pos}>} selections 2116 * @return {boolean} 2117 */ 2118 _validateSelectionsContexts: function(mainSelection, selections) 2119 { 2120 var mainSelectionContext = this._textEditor.copyRange(mainSelection); 2121 for (var i = 0; i < selections.length; ++i) { 2122 var wordRange = this._autocompleteWordRange(selections[i].head.line, selections[i].head.ch); 2123 if (!wordRange) 2124 return false; 2125 var context = this._textEditor.copyRange(wordRange); 2126 if (context !== mainSelectionContext) 2127 return false; 2128 } 2129 return true; 2130 }, 2131 2132 autocomplete: function() 2133 { 2134 var dictionary = this._dictionary; 2135 if (this._codeMirror.somethingSelected()) { 2136 this.finishAutocomplete(); 2137 return; 2138 } 2139 2140 var selections = this._codeMirror.listSelections().slice(); 2141 var topSelection = selections.shift(); 2142 var cursor = topSelection.head; 2143 var substituteRange = this._autocompleteWordRange(cursor.line, cursor.ch); 2144 if (!substituteRange || substituteRange.startColumn === cursor.ch || !this._validateSelectionsContexts(substituteRange, selections)) { 2145 this.finishAutocomplete(); 2146 return; 2147 } 2148 2149 var prefixRange = substituteRange.clone(); 2150 prefixRange.endColumn = cursor.ch; 2151 2152 var substituteWord = this._textEditor.copyRange(substituteRange); 2153 var hasPrefixInDictionary = dictionary.hasWord(substituteWord); 2154 if (hasPrefixInDictionary) 2155 dictionary.removeWord(substituteWord); 2156 var wordsWithPrefix = dictionary.wordsWithPrefix(this._textEditor.copyRange(prefixRange)); 2157 if (hasPrefixInDictionary) 2158 dictionary.addWord(substituteWord); 2159 2160 function sortSuggestions(a, b) 2161 { 2162 return dictionary.wordCount(b) - dictionary.wordCount(a) || a.length - b.length; 2163 } 2164 2165 wordsWithPrefix.sort(sortSuggestions); 2166 2167 if (!this._suggestBox) 2168 this._suggestBox = new WebInspector.SuggestBox(this, 6); 2169 var oldPrefixRange = this._prefixRange; 2170 this._prefixRange = prefixRange; 2171 if (!oldPrefixRange || prefixRange.startLine !== oldPrefixRange.startLine || prefixRange.startColumn !== oldPrefixRange.startColumn) 2172 this._updateAnchorBox(); 2173 this._suggestBox.updateSuggestions(this._anchorBox, wordsWithPrefix, 0, true, this._textEditor.copyRange(prefixRange)); 2174 if (!this._suggestBox.visible()) 2175 this.finishAutocomplete(); 2176 }, 2177 2178 finishAutocomplete: function() 2179 { 2180 if (!this._suggestBox) 2181 return; 2182 this._suggestBox.hide(); 2183 this._suggestBox = null; 2184 this._prefixRange = null; 2185 this._anchorBox = null; 2186 }, 2187 2188 /** 2189 * @param {!Event} e 2190 * @return {boolean} 2191 */ 2192 keyDown: function(e) 2193 { 2194 if (!this._suggestBox) 2195 return false; 2196 if (e.keyCode === WebInspector.KeyboardShortcut.Keys.Esc.code) { 2197 this.finishAutocomplete(); 2198 return true; 2199 } 2200 if (e.keyCode === WebInspector.KeyboardShortcut.Keys.Tab.code) { 2201 this._suggestBox.acceptSuggestion(); 2202 this.finishAutocomplete(); 2203 return true; 2204 } 2205 return this._suggestBox.keyPressed(e); 2206 }, 2207 2208 /** 2209 * @param {string} suggestion 2210 * @param {boolean=} isIntermediateSuggestion 2211 */ 2212 applySuggestion: function(suggestion, isIntermediateSuggestion) 2213 { 2214 this._currentSuggestion = suggestion; 2215 }, 2216 2217 acceptSuggestion: function() 2218 { 2219 if (this._prefixRange.endColumn - this._prefixRange.startColumn === this._currentSuggestion.length) 2220 return; 2221 2222 var selections = this._codeMirror.listSelections().slice(); 2223 var prefixLength = this._prefixRange.endColumn - this._prefixRange.startColumn; 2224 for (var i = selections.length - 1; i >= 0; --i) { 2225 var start = selections[i].head; 2226 var end = new CodeMirror.Pos(start.line, start.ch - prefixLength); 2227 this._codeMirror.replaceRange(this._currentSuggestion, start, end, "+autocomplete"); 2228 } 2229 }, 2230 2231 _onScroll: function() 2232 { 2233 if (!this._suggestBox) 2234 return; 2235 var cursor = this._codeMirror.getCursor(); 2236 var scrollInfo = this._codeMirror.getScrollInfo(); 2237 var topmostLineNumber = this._codeMirror.lineAtHeight(scrollInfo.top, "local"); 2238 var bottomLine = this._codeMirror.lineAtHeight(scrollInfo.top + scrollInfo.clientHeight, "local"); 2239 if (cursor.line < topmostLineNumber || cursor.line > bottomLine) 2240 this.finishAutocomplete(); 2241 else { 2242 this._updateAnchorBox(); 2243 this._suggestBox.setPosition(this._anchorBox); 2244 } 2245 }, 2246 2247 _onCursorActivity: function() 2248 { 2249 if (!this._suggestBox) 2250 return; 2251 var cursor = this._codeMirror.getCursor(); 2252 if (cursor.line !== this._prefixRange.startLine || cursor.ch > this._prefixRange.endColumn || cursor.ch <= this._prefixRange.startColumn) 2253 this.finishAutocomplete(); 2254 }, 2255 2256 _updateAnchorBox: function() 2257 { 2258 var line = this._prefixRange.startLine; 2259 var column = this._prefixRange.startColumn; 2260 var metrics = this._textEditor.cursorPositionToCoordinates(line, column); 2261 this._anchorBox = metrics ? new AnchorBox(metrics.x, metrics.y, 0, metrics.height) : null; 2262 }, 2263 } 2264 2265 /** 2266 * @constructor 2267 * @param {!WebInspector.CodeMirrorTextEditor} textEditor 2268 * @param {!CodeMirror} codeMirror 2269 */ 2270 WebInspector.CodeMirrorTextEditor.SelectNextOccurrenceController = function(textEditor, codeMirror) 2271 { 2272 this._textEditor = textEditor; 2273 this._codeMirror = codeMirror; 2274 } 2275 2276 WebInspector.CodeMirrorTextEditor.SelectNextOccurrenceController.prototype = { 2277 selectionWillChange: function() 2278 { 2279 if (!this._muteSelectionListener) 2280 delete this._fullWordSelection; 2281 }, 2282 2283 /** 2284 * @param {!Array.<!WebInspector.TextRange>} selections 2285 * @param {!WebInspector.TextRange} range 2286 * @return {boolean} 2287 */ 2288 _findRange: function(selections, range) 2289 { 2290 for (var i = 0; i < selections.length; ++i) { 2291 if (range.equal(selections[i])) 2292 return true; 2293 } 2294 return false; 2295 }, 2296 2297 undoLastSelection: function() 2298 { 2299 this._muteSelectionListener = true; 2300 this._codeMirror.execCommand("undoSelection"); 2301 this._muteSelectionListener = false; 2302 }, 2303 2304 selectNextOccurrence: function() 2305 { 2306 var selections = this._textEditor.selections(); 2307 var anyEmptySelection = false; 2308 for (var i = 0; i < selections.length; ++i) { 2309 var selection = selections[i]; 2310 anyEmptySelection = anyEmptySelection || selection.isEmpty(); 2311 if (selection.startLine !== selection.endLine) 2312 return; 2313 } 2314 if (anyEmptySelection) { 2315 this._expandSelectionsToWords(selections); 2316 return; 2317 } 2318 2319 var last = selections[selections.length - 1]; 2320 var next = last; 2321 do { 2322 next = this._findNextOccurrence(next, !!this._fullWordSelection); 2323 } while (next && this._findRange(selections, next) && !next.equal(last)); 2324 2325 if (!next) 2326 return; 2327 selections.push(next); 2328 2329 this._muteSelectionListener = true; 2330 this._textEditor.setSelections(selections, selections.length - 1); 2331 delete this._muteSelectionListener; 2332 2333 this._textEditor._revealLine(next.startLine); 2334 }, 2335 2336 /** 2337 * @param {!Array.<!WebInspector.TextRange>} selections 2338 */ 2339 _expandSelectionsToWords: function(selections) 2340 { 2341 var newSelections = []; 2342 for (var i = 0; i < selections.length; ++i) { 2343 var selection = selections[i]; 2344 var startRangeWord = this._textEditor._wordRangeForCursorPosition(selection.startLine, selection.startColumn, WebInspector.TextUtils.isWordChar) 2345 || WebInspector.TextRange.createFromLocation(selection.startLine, selection.startColumn); 2346 var endRangeWord = this._textEditor._wordRangeForCursorPosition(selection.endLine, selection.endColumn, WebInspector.TextUtils.isWordChar) 2347 || WebInspector.TextRange.createFromLocation(selection.endLine, selection.endColumn); 2348 var newSelection = new WebInspector.TextRange(startRangeWord.startLine, startRangeWord.startColumn, endRangeWord.endLine, endRangeWord.endColumn); 2349 newSelections.push(newSelection); 2350 } 2351 this._textEditor.setSelections(newSelections, newSelections.length - 1); 2352 this._fullWordSelection = true; 2353 }, 2354 2355 /** 2356 * @param {!WebInspector.TextRange} range 2357 * @param {boolean} fullWord 2358 * @return {?WebInspector.TextRange} 2359 */ 2360 _findNextOccurrence: function(range, fullWord) 2361 { 2362 range = range.normalize(); 2363 var matchedLineNumber; 2364 var matchedColumnNumber; 2365 var textToFind = this._textEditor.copyRange(range); 2366 function findWordInLine(wordRegex, lineNumber, lineText, from, to) 2367 { 2368 if (typeof matchedLineNumber === "number") 2369 return true; 2370 wordRegex.lastIndex = from; 2371 var result = wordRegex.exec(lineText); 2372 if (!result || result.index + textToFind.length > to) 2373 return false; 2374 matchedLineNumber = lineNumber; 2375 matchedColumnNumber = result.index; 2376 return true; 2377 } 2378 2379 var iteratedLineNumber; 2380 function lineIterator(regex, lineHandle) 2381 { 2382 if (findWordInLine(regex, iteratedLineNumber++, lineHandle.text, 0, lineHandle.text.length)) 2383 return true; 2384 } 2385 2386 var regexSource = textToFind.escapeForRegExp(); 2387 if (fullWord) 2388 regexSource = "\\b" + regexSource + "\\b"; 2389 var wordRegex = new RegExp(regexSource, "g"); 2390 var currentLineText = this._codeMirror.getLine(range.startLine); 2391 2392 findWordInLine(wordRegex, range.startLine, currentLineText, range.endColumn, currentLineText.length); 2393 iteratedLineNumber = range.startLine + 1; 2394 this._codeMirror.eachLine(range.startLine + 1, this._codeMirror.lineCount(), lineIterator.bind(null, wordRegex)); 2395 iteratedLineNumber = 0; 2396 this._codeMirror.eachLine(0, range.startLine, lineIterator.bind(null, wordRegex)); 2397 findWordInLine(wordRegex, range.startLine, currentLineText, 0, range.startColumn); 2398 2399 if (typeof matchedLineNumber !== "number") 2400 return null; 2401 return new WebInspector.TextRange(matchedLineNumber, matchedColumnNumber, matchedLineNumber, matchedColumnNumber + textToFind.length); 2402 } 2403 } 2404 2405 /** 2406 * @param {string} modeName 2407 * @param {string} tokenPrefix 2408 */ 2409 WebInspector.CodeMirrorTextEditor._overrideModeWithPrefixedTokens = function(modeName, tokenPrefix) 2410 { 2411 var oldModeName = modeName + "-old"; 2412 if (CodeMirror.modes[oldModeName]) 2413 return; 2414 2415 CodeMirror.defineMode(oldModeName, CodeMirror.modes[modeName]); 2416 CodeMirror.defineMode(modeName, modeConstructor); 2417 2418 function modeConstructor(config, parserConfig) 2419 { 2420 var innerConfig = {}; 2421 for (var i in parserConfig) 2422 innerConfig[i] = parserConfig[i]; 2423 innerConfig.name = oldModeName; 2424 var codeMirrorMode = CodeMirror.getMode(config, innerConfig); 2425 codeMirrorMode.name = modeName; 2426 codeMirrorMode.token = tokenOverride.bind(null, codeMirrorMode.token); 2427 return codeMirrorMode; 2428 } 2429 2430 function tokenOverride(superToken, stream, state) 2431 { 2432 var token = superToken(stream, state); 2433 return token ? tokenPrefix + token.split(/ +/).join(" " + tokenPrefix) : token; 2434 } 2435 } 2436 2437 WebInspector.CodeMirrorTextEditor._overrideModeWithPrefixedTokens("css", "css-"); 2438 WebInspector.CodeMirrorTextEditor._overrideModeWithPrefixedTokens("javascript", "js-"); 2439 WebInspector.CodeMirrorTextEditor._overrideModeWithPrefixedTokens("xml", "xml-"); 2440 2441 (function() { 2442 var backgroundColor = InspectorFrontendHost.getSelectionBackgroundColor(); 2443 var backgroundColorRule = backgroundColor ? ".CodeMirror .CodeMirror-selected { background-color: " + backgroundColor + ";}" : ""; 2444 var foregroundColor = InspectorFrontendHost.getSelectionForegroundColor(); 2445 var foregroundColorRule = foregroundColor ? ".CodeMirror .CodeMirror-selectedtext:not(.CodeMirror-persist-highlight) { color: " + foregroundColor + "!important;}" : ""; 2446 if (!foregroundColorRule && !backgroundColorRule) 2447 return; 2448 2449 var style = document.createElement("style"); 2450 style.textContent = backgroundColorRule + foregroundColorRule; 2451 document.head.appendChild(style); 2452 })(); 2453