1 /* 2 * Copyright (C) 2009 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 * @param {WebInspector.TextRange} newRange 34 * @param {string} originalText 35 * @param {WebInspector.TextRange} originalSelection 36 */ 37 WebInspector.TextEditorCommand = function(newRange, originalText, originalSelection) 38 { 39 this.newRange = newRange; 40 this.originalText = originalText; 41 this.originalSelection = originalSelection; 42 } 43 44 /** 45 * @constructor 46 * @extends {WebInspector.Object} 47 */ 48 WebInspector.TextEditorModel = function() 49 { 50 this._lines = [""]; 51 this._attributes = []; 52 /** @type {Array.<WebInspector.TextEditorCommand>} */ 53 this._undoStack = []; 54 this._noPunctuationRegex = /[^ !%&()*+,-.:;<=>?\[\]\^{|}~]+/; 55 this._lineBreak = "\n"; 56 } 57 58 WebInspector.TextEditorModel.Events = { 59 TextChanged: "TextChanged" 60 } 61 62 WebInspector.TextEditorModel.endsWithBracketRegex = /[{(\[]\s*$/; 63 64 WebInspector.TextEditorModel.prototype = { 65 /** 66 * @return {boolean} 67 */ 68 isClean: function() 69 { 70 return this._cleanState === this._undoStack.length; 71 }, 72 73 markClean: function() 74 { 75 this._cleanState = this._undoStack.length; 76 }, 77 78 /** 79 * @return {number} 80 */ 81 get linesCount() 82 { 83 return this._lines.length; 84 }, 85 86 /** 87 * @return {string} 88 */ 89 text: function() 90 { 91 return this._lines.join(this._lineBreak); 92 }, 93 94 /** 95 * @return {WebInspector.TextRange} 96 */ 97 range: function() 98 { 99 return new WebInspector.TextRange(0, 0, this._lines.length - 1, this._lines[this._lines.length - 1].length); 100 }, 101 102 /** 103 * @return {string} 104 */ 105 get lineBreak() 106 { 107 return this._lineBreak; 108 }, 109 110 /** 111 * @param {number} lineNumber 112 * @return {string} 113 */ 114 line: function(lineNumber) 115 { 116 if (lineNumber >= this._lines.length) 117 throw "Out of bounds:" + lineNumber; 118 return this._lines[lineNumber]; 119 }, 120 121 /** 122 * @param {number} lineNumber 123 * @return {number} 124 */ 125 lineLength: function(lineNumber) 126 { 127 return this._lines[lineNumber].length; 128 }, 129 130 /** 131 * @param {string} text 132 */ 133 setText: function(text) 134 { 135 this._resetUndoStack(); 136 text = text || ""; 137 this._attributes = []; 138 var range = this.range(); 139 this._lineBreak = /\r\n/.test(text) ? "\r\n" : "\n"; 140 var newRange = this._innerSetText(range, text); 141 this.dispatchEventToListeners(WebInspector.TextEditorModel.Events.TextChanged, { oldRange: range, newRange: newRange}); 142 }, 143 144 /** 145 * @param {WebInspector.TextRange} range 146 * @return {boolean} 147 */ 148 _rangeHasOneCharacter: function(range) 149 { 150 if (range.startLine === range.endLine && range.endColumn - range.startColumn === 1) 151 return true; 152 if (range.endLine - range.startLine === 1 && range.endColumn === 0 && range.startColumn === this.lineLength(range.startLine)) 153 return true; 154 return false; 155 }, 156 157 /** 158 * @param {WebInspector.TextRange} range 159 * @param {string} text 160 * @param {WebInspector.TextRange=} originalSelection 161 * @return {boolean} 162 */ 163 _isEditRangeUndoBoundary: function(range, text, originalSelection) 164 { 165 if (originalSelection && !originalSelection.isEmpty()) 166 return true; 167 if (text) 168 return text.length > 1 || !range.isEmpty(); 169 return !this._rangeHasOneCharacter(range); 170 }, 171 172 /** 173 * @param {WebInspector.TextRange} range 174 * @param {string} text 175 * @return {boolean} 176 */ 177 _isEditRangeAdjacentToLastCommand: function(range, text) 178 { 179 if (!this._lastCommand) 180 return true; 181 if (!text) { 182 // FIXME: Distinguish backspace and delete in lastCommand. 183 return this._lastCommand.newRange.immediatelyPrecedes(range) || this._lastCommand.newRange.immediatelyFollows(range); 184 } 185 return text.indexOf("\n") === -1 && this._lastCommand.newRange.immediatelyPrecedes(range); 186 }, 187 188 /** 189 * @param {WebInspector.TextRange} range 190 * @param {string} text 191 * @param {WebInspector.TextRange=} originalSelection 192 * @return {WebInspector.TextRange} 193 */ 194 editRange: function(range, text, originalSelection) 195 { 196 var undoBoundary = this._isEditRangeUndoBoundary(range, text, originalSelection); 197 if (undoBoundary || !this._isEditRangeAdjacentToLastCommand(range, text)) 198 this._markUndoableState(); 199 var newRange = this._innerEditRange(range, text, originalSelection); 200 if (undoBoundary) 201 this._markUndoableState(); 202 return newRange; 203 }, 204 205 /** 206 * @param {WebInspector.TextRange} range 207 * @param {string} text 208 * @param {WebInspector.TextRange=} originalSelection 209 * @return {WebInspector.TextRange} 210 */ 211 _innerEditRange: function(range, text, originalSelection) 212 { 213 var originalText = this.copyRange(range); 214 var newRange = this._innerSetText(range, text); 215 this._lastCommand = this._pushUndoableCommand(newRange, originalText, originalSelection || range); 216 this.dispatchEventToListeners(WebInspector.TextEditorModel.Events.TextChanged, { oldRange: range, newRange: newRange, editRange: true }); 217 return newRange; 218 }, 219 220 /** 221 * @param {WebInspector.TextRange} range 222 * @param {string} text 223 * @return {WebInspector.TextRange} 224 */ 225 _innerSetText: function(range, text) 226 { 227 this._eraseRange(range); 228 if (text === "") 229 return new WebInspector.TextRange(range.startLine, range.startColumn, range.startLine, range.startColumn); 230 231 var newLines = text.split(/\r?\n/); 232 233 var prefix = this._lines[range.startLine].substring(0, range.startColumn); 234 var suffix = this._lines[range.startLine].substring(range.startColumn); 235 236 var postCaret = prefix.length; 237 // Insert text. 238 if (newLines.length === 1) { 239 this._setLine(range.startLine, prefix + newLines[0] + suffix); 240 postCaret += newLines[0].length; 241 } else { 242 this._setLine(range.startLine, prefix + newLines[0]); 243 this._insertLines(range, newLines); 244 this._setLine(range.startLine + newLines.length - 1, newLines[newLines.length - 1] + suffix); 245 postCaret = newLines[newLines.length - 1].length; 246 } 247 248 return new WebInspector.TextRange(range.startLine, range.startColumn, 249 range.startLine + newLines.length - 1, postCaret); 250 }, 251 252 /** 253 * @param {WebInspector.TextRange} range 254 * @param {Array.<string>} newLines 255 */ 256 _insertLines: function(range, newLines) 257 { 258 var lines = new Array(this._lines.length + newLines.length - 1); 259 for (var i = 0; i <= range.startLine; ++i) 260 lines[i] = this._lines[i]; 261 // Line at [0] is already set via setLine. 262 for (var i = 1; i < newLines.length; ++i) 263 lines[range.startLine + i] = newLines[i]; 264 for (var i = range.startLine + newLines.length; i < lines.length; ++i) 265 lines[i] = this._lines[i - newLines.length + 1]; 266 this._lines = lines; 267 268 // Adjust attributes, attributes move with the first character of line. 269 var attributes = new Array(lines.length); 270 var insertionIndex = range.startColumn ? range.startLine + 1 : range.startLine; 271 for (var i = 0; i < insertionIndex; ++i) 272 attributes[i] = this._attributes[i]; 273 for (var i = insertionIndex + newLines.length - 1; i < attributes.length; ++i) 274 attributes[i] = this._attributes[i - newLines.length + 1]; 275 this._attributes = attributes; 276 }, 277 278 /** 279 * @param {WebInspector.TextRange} range 280 */ 281 _eraseRange: function(range) 282 { 283 if (range.isEmpty()) 284 return; 285 286 var prefix = this._lines[range.startLine].substring(0, range.startColumn); 287 var suffix = this._lines[range.endLine].substring(range.endColumn); 288 289 if (range.endLine > range.startLine) { 290 this._lines.splice(range.startLine + 1, range.endLine - range.startLine); 291 // Adjust attributes, attributes move with the first character of line. 292 this._attributes.splice(range.startColumn ? range.startLine + 1 : range.startLine, range.endLine - range.startLine); 293 } 294 this._setLine(range.startLine, prefix + suffix); 295 }, 296 297 /** 298 * @param {number} lineNumber 299 * @param {string} text 300 */ 301 _setLine: function(lineNumber, text) 302 { 303 this._lines[lineNumber] = text; 304 }, 305 306 /** 307 * @param {number} lineNumber 308 * @param {number} column 309 * @return {WebInspector.TextRange} 310 */ 311 wordRange: function(lineNumber, column) 312 { 313 return new WebInspector.TextRange(lineNumber, this.wordStart(lineNumber, column, true), lineNumber, this.wordEnd(lineNumber, column, true)); 314 }, 315 316 /** 317 * @param {number} lineNumber 318 * @param {number} column 319 * @param {boolean} gapless 320 * @return {number} 321 */ 322 wordStart: function(lineNumber, column, gapless) 323 { 324 var line = this._lines[lineNumber]; 325 var prefix = line.substring(0, column).split("").reverse().join(""); 326 var prefixMatch = this._noPunctuationRegex.exec(prefix); 327 return prefixMatch && (!gapless || prefixMatch.index === 0) ? column - prefixMatch.index - prefixMatch[0].length : column; 328 }, 329 330 /** 331 * @param {number} lineNumber 332 * @param {number} column 333 * @param {boolean} gapless 334 * @return {number} 335 */ 336 wordEnd: function(lineNumber, column, gapless) 337 { 338 var line = this._lines[lineNumber]; 339 var suffix = line.substring(column); 340 var suffixMatch = this._noPunctuationRegex.exec(suffix); 341 return suffixMatch && (!gapless || suffixMatch.index === 0) ? column + suffixMatch.index + suffixMatch[0].length : column; 342 }, 343 344 /** 345 * @param {WebInspector.TextRange} range 346 * @return {string} 347 */ 348 copyRange: function(range) 349 { 350 if (!range) 351 range = this.range(); 352 353 var clip = []; 354 if (range.startLine === range.endLine) { 355 clip.push(this._lines[range.startLine].substring(range.startColumn, range.endColumn)); 356 return clip.join(this._lineBreak); 357 } 358 clip.push(this._lines[range.startLine].substring(range.startColumn)); 359 for (var i = range.startLine + 1; i < range.endLine; ++i) 360 clip.push(this._lines[i]); 361 clip.push(this._lines[range.endLine].substring(0, range.endColumn)); 362 return clip.join(this._lineBreak); 363 }, 364 365 /** 366 * @param {number} line 367 * @param {string} name 368 * @param {Object?} value 369 */ 370 setAttribute: function(line, name, value) 371 { 372 var attrs = this._attributes[line]; 373 if (!attrs) { 374 attrs = {}; 375 this._attributes[line] = attrs; 376 } 377 attrs[name] = value; 378 }, 379 380 /** 381 * @param {number} line 382 * @param {string} name 383 * @return {Object|null} value 384 */ 385 getAttribute: function(line, name) 386 { 387 var attrs = this._attributes[line]; 388 return attrs ? attrs[name] : null; 389 }, 390 391 /** 392 * @param {number} line 393 * @param {string} name 394 */ 395 removeAttribute: function(line, name) 396 { 397 var attrs = this._attributes[line]; 398 if (attrs) 399 delete attrs[name]; 400 }, 401 402 /** 403 * @param {WebInspector.TextRange} newRange 404 * @param {string} originalText 405 * @param {WebInspector.TextRange} originalSelection 406 * @return {WebInspector.TextEditorCommand} 407 */ 408 _pushUndoableCommand: function(newRange, originalText, originalSelection) 409 { 410 var command = new WebInspector.TextEditorCommand(newRange.clone(), originalText, originalSelection); 411 if (this._inUndo) 412 this._redoStack.push(command); 413 else { 414 if (!this._inRedo) { 415 this._redoStack = []; 416 if (typeof this._cleanState === "number" && this._cleanState > this._undoStack.length) 417 delete this._cleanState; 418 } 419 this._undoStack.push(command); 420 } 421 return command; 422 }, 423 424 /** 425 * @return {?WebInspector.TextRange} 426 */ 427 undo: function() 428 { 429 if (!this._undoStack.length) 430 return null; 431 432 this._markRedoableState(); 433 434 this._inUndo = true; 435 var range = this._doUndo(this._undoStack); 436 delete this._inUndo; 437 438 return range; 439 }, 440 441 /** 442 * @return {WebInspector.TextRange} 443 */ 444 redo: function() 445 { 446 if (!this._redoStack || !this._redoStack.length) 447 return null; 448 this._markUndoableState(); 449 450 this._inRedo = true; 451 var range = this._doUndo(this._redoStack); 452 delete this._inRedo; 453 454 return range ? range.collapseToEnd() : null; 455 }, 456 457 /** 458 * @param {Array.<WebInspector.TextEditorCommand>} stack 459 * @return {WebInspector.TextRange} 460 */ 461 _doUndo: function(stack) 462 { 463 var range = null; 464 for (var i = stack.length - 1; i >= 0; --i) { 465 var command = stack[i]; 466 stack.length = i; 467 this._innerEditRange(command.newRange, command.originalText); 468 range = command.originalSelection; 469 if (i > 0 && stack[i - 1].explicit) 470 return range; 471 } 472 return range; 473 }, 474 475 _markUndoableState: function() 476 { 477 if (this._undoStack.length) 478 this._undoStack[this._undoStack.length - 1].explicit = true; 479 }, 480 481 _markRedoableState: function() 482 { 483 if (this._redoStack.length) 484 this._redoStack[this._redoStack.length - 1].explicit = true; 485 }, 486 487 _resetUndoStack: function() 488 { 489 delete this._cleanState; 490 this._undoStack = []; 491 this._redoStack = []; 492 }, 493 494 /** 495 * @param {WebInspector.TextRange} range 496 * @return {WebInspector.TextRange} 497 */ 498 indentLines: function(range) 499 { 500 this._markUndoableState(); 501 502 var indent = WebInspector.settings.textEditorIndent.get(); 503 var newRange = range.clone(); 504 // Do not change a selection start position when it is at the beginning of a line 505 if (range.startColumn) 506 newRange.startColumn += indent.length; 507 508 var indentEndLine = range.endLine; 509 if (range.endColumn) 510 newRange.endColumn += indent.length; 511 else 512 indentEndLine--; 513 514 for (var lineNumber = range.startLine; lineNumber <= indentEndLine; lineNumber++) 515 this._innerEditRange(WebInspector.TextRange.createFromLocation(lineNumber, 0), indent); 516 517 return newRange; 518 }, 519 520 /** 521 * @param {WebInspector.TextRange} range 522 * @return {WebInspector.TextRange} 523 */ 524 unindentLines: function(range) 525 { 526 this._markUndoableState(); 527 528 var indent = WebInspector.settings.textEditorIndent.get(); 529 var indentLength = indent === WebInspector.TextUtils.Indent.TabCharacter ? 4 : indent.length; 530 var lineIndentRegex = new RegExp("^ {1," + indentLength + "}"); 531 var newRange = range.clone(); 532 533 var indentEndLine = range.endLine; 534 if (!range.endColumn) 535 indentEndLine--; 536 537 for (var lineNumber = range.startLine; lineNumber <= indentEndLine; lineNumber++) { 538 var line = this.line(lineNumber); 539 var firstCharacter = line.charAt(0); 540 var lineIndentLength; 541 542 if (firstCharacter === " ") 543 lineIndentLength = line.match(lineIndentRegex)[0].length; 544 else if (firstCharacter === "\t") 545 lineIndentLength = 1; 546 else 547 continue; 548 549 this._innerEditRange(new WebInspector.TextRange(lineNumber, 0, lineNumber, lineIndentLength), ""); 550 551 if (lineNumber === range.startLine) 552 newRange.startColumn = Math.max(0, newRange.startColumn - lineIndentLength); 553 if (lineNumber === range.endLine) 554 newRange.endColumn = Math.max(0, newRange.endColumn - lineIndentLength); 555 } 556 557 return newRange; 558 }, 559 560 /** 561 * @param {number=} from 562 * @param {number=} to 563 * @return {WebInspector.TextEditorModel} 564 */ 565 slice: function(from, to) 566 { 567 var textModel = new WebInspector.TextEditorModel(); 568 textModel._lines = this._lines.slice(from, to); 569 textModel._lineBreak = this._lineBreak; 570 return textModel; 571 }, 572 573 /** 574 * @param {WebInspector.TextRange} range 575 * @return {WebInspector.TextRange} 576 */ 577 growRangeLeft: function(range) 578 { 579 var result = range.clone(); 580 if (result.startColumn) 581 --result.startColumn; 582 else if (result.startLine) 583 result.startColumn = this.lineLength(--result.startLine); 584 return result; 585 }, 586 587 /** 588 * @param {WebInspector.TextRange} range 589 * @return {WebInspector.TextRange} 590 */ 591 growRangeRight: function(range) 592 { 593 var result = range.clone(); 594 if (result.endColumn < this.lineLength(result.endLine)) 595 ++result.endColumn; 596 else if (result.endLine < this.linesCount) { 597 result.endColumn = 0; 598 ++result.endLine; 599 } 600 return result; 601 }, 602 603 __proto__: WebInspector.Object.prototype 604 } 605 606 /** 607 * @constructor 608 * @param {WebInspector.TextEditorModel} textModel 609 */ 610 WebInspector.TextEditorModel.BraceMatcher = function(textModel) 611 { 612 this._textModel = textModel; 613 } 614 615 WebInspector.TextEditorModel.BraceMatcher.prototype = { 616 /** 617 * @param {number} lineNumber 618 * @return {Array.<{startColumn: number, endColumn: number, token: string}>} 619 */ 620 _braceRanges: function(lineNumber) 621 { 622 if (lineNumber >= this._textModel.linesCount || lineNumber < 0) 623 return null; 624 625 var attribute = this._textModel.getAttribute(lineNumber, "highlight"); 626 if (!attribute) 627 return null; 628 else 629 return attribute.braces; 630 }, 631 632 /** 633 * @param {string} braceTokenLeft 634 * @param {string} braceTokenRight 635 * @return {boolean} 636 */ 637 _matches: function(braceTokenLeft, braceTokenRight) 638 { 639 return ((braceTokenLeft === "brace-start" && braceTokenRight === "brace-end") || (braceTokenLeft === "block-start" && braceTokenRight === "block-end")); 640 }, 641 642 /** 643 * @param {number} lineNumber 644 * @param {number} column 645 * @param {number=} maxBraceIteration 646 * @return {?{lineNumber: number, column: number, token: string}} 647 */ 648 findLeftCandidate: function(lineNumber, column, maxBraceIteration) 649 { 650 var braces = this._braceRanges(lineNumber); 651 if (!braces) 652 return null; 653 654 var braceIndex = braces.length - 1; 655 while (braceIndex >= 0 && braces[braceIndex].startColumn > column) 656 --braceIndex; 657 658 var brace = braceIndex >= 0 ? braces[braceIndex] : null; 659 if (brace && brace.startColumn === column && (brace.token === "block-end" || brace.token === "brace-end")) 660 --braceIndex; 661 662 var stack = []; 663 maxBraceIteration = maxBraceIteration || Number.MAX_VALUE; 664 while (--maxBraceIteration) { 665 if (braceIndex < 0) { 666 while ((braces = this._braceRanges(--lineNumber)) && !braces.length) {}; 667 if (!braces) 668 return null; 669 braceIndex = braces.length - 1; 670 } 671 brace = braces[braceIndex]; 672 if (brace.token === "block-end" || brace.token === "brace-end") 673 stack.push(brace.token); 674 else if (stack.length === 0) 675 return { 676 lineNumber: lineNumber, 677 column: brace.startColumn, 678 token: brace.token 679 }; 680 else if (!this._matches(brace.token, stack.pop())) 681 return null; 682 683 --braceIndex; 684 } 685 return null; 686 }, 687 688 /** 689 * @param {number} lineNumber 690 * @param {number} column 691 * @param {number=} maxBraceIteration 692 * @return {?{lineNumber: number, column: number, token: string}} 693 */ 694 findRightCandidate: function(lineNumber, column, maxBraceIteration) 695 { 696 var braces = this._braceRanges(lineNumber); 697 if (!braces) 698 return null; 699 700 var braceIndex = 0; 701 while (braceIndex < braces.length && braces[braceIndex].startColumn < column) 702 ++braceIndex; 703 704 var brace = braceIndex < braces.length ? braces[braceIndex] : null; 705 if (brace && brace.startColumn === column && (brace.token === "block-start" || brace.token === "brace-start")) 706 ++braceIndex; 707 708 var stack = []; 709 maxBraceIteration = maxBraceIteration || Number.MAX_VALUE; 710 while (--maxBraceIteration) { 711 if (braceIndex >= braces.length) { 712 while ((braces = this._braceRanges(++lineNumber)) && !braces.length) {}; 713 if (!braces) 714 return null; 715 braceIndex = 0; 716 } 717 brace = braces[braceIndex]; 718 if (brace.token === "block-start" || brace.token === "brace-start") 719 stack.push(brace.token); 720 else if (stack.length === 0) 721 return { 722 lineNumber: lineNumber, 723 column: brace.startColumn, 724 token: brace.token 725 }; 726 else if (!this._matches(stack.pop(), brace.token)) 727 return null; 728 ++braceIndex; 729 } 730 return null; 731 }, 732 733 /** 734 * @param {number} lineNumber 735 * @param {number} column 736 * @param {number=} maxBraceIteration 737 * @return {?{leftBrace: {lineNumber: number, column: number, token: string}, rightBrace: {lineNumber: number, column: number, token: string}}} 738 */ 739 enclosingBraces: function(lineNumber, column, maxBraceIteration) 740 { 741 var leftBraceLocation = this.findLeftCandidate(lineNumber, column, maxBraceIteration); 742 if (!leftBraceLocation) 743 return null; 744 745 var rightBraceLocation = this.findRightCandidate(lineNumber, column, maxBraceIteration); 746 if (!rightBraceLocation) 747 return null; 748 749 if (!this._matches(leftBraceLocation.token, rightBraceLocation.token)) 750 return null; 751 752 return { 753 leftBrace: leftBraceLocation, 754 rightBrace: rightBraceLocation 755 }; 756 }, 757 } 758