TextEditorModel.js 27 KB

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