| OLD | NEW |
| (Empty) |
| 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, originalSelect
ion) | |
| 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._li
nes[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.TextCh
anged, { 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.startCo
lumn === 1) | |
| 151 return true; | |
| 152 if (range.endLine - range.startLine === 1 && range.endColumn === 0 && ra
nge.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.immediate
lyPrecedes(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, originalSe
lection); | |
| 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, or
iginalSelection || range); | |
| 216 this.dispatchEventToListeners(WebInspector.TextEditorModel.Events.TextCh
anged, { 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[newLin
es.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.sta
rtLine; | |
| 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.startL
ine); | |
| 291 // Adjust attributes, attributes move with the first character of li
ne. | |
| 292 this._attributes.splice(range.startColumn ? range.startLine + 1 : ra
nge.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 - p
refixMatch.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 + s
uffixMatch.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(), origi
nalText, 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 > t
his._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; line
Number++) | |
| 515 this._innerEditRange(WebInspector.TextRange.createFromLocation(lineN
umber, 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; line
Number++) { | |
| 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, lineN
umber, lineIndentLength), ""); | |
| 550 | |
| 551 if (lineNumber === range.startLine) | |
| 552 newRange.startColumn = Math.max(0, newRange.startColumn - lineIn
dentLength); | |
| 553 if (lineNumber === range.endLine) | |
| 554 newRange.endColumn = Math.max(0, newRange.endColumn - lineIndent
Length); | |
| 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.len
gth) {}; | |
| 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 < co
lumn) | |
| 702 ++braceIndex; | |
| 703 | |
| 704 var brace = braceIndex < braces.length ? braces[braceIndex] : null; | |
| 705 if (brace && brace.startColumn === column && (brace.token === "block-sta
rt" || 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.len
gth) {}; | |
| 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, maxBr
aceIteration); | |
| 742 if (!leftBraceLocation) | |
| 743 return null; | |
| 744 | |
| 745 var rightBraceLocation = this.findRightCandidate(lineNumber, column, max
BraceIteration); | |
| 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 } | |
| OLD | NEW |