folding.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857
  1. /* ***** BEGIN LICENSE BLOCK *****
  2. * Distributed under the BSD license:
  3. *
  4. * Copyright (c) 2010, Ajax.org B.V.
  5. * All rights reserved.
  6. *
  7. * Redistribution and use in source and binary forms, with or without
  8. * modification, are permitted provided that the following conditions are met:
  9. * * Redistributions of source code must retain the above copyright
  10. * notice, this list of conditions and the following disclaimer.
  11. * * Redistributions in binary form must reproduce the above copyright
  12. * notice, this list of conditions and the following disclaimer in the
  13. * documentation and/or other materials provided with the distribution.
  14. * * Neither the name of Ajax.org B.V. nor the
  15. * names of its contributors may be used to endorse or promote products
  16. * derived from this software without specific prior written permission.
  17. *
  18. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  19. * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  20. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  21. * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
  22. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  23. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  24. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  25. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  26. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  27. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28. *
  29. * ***** END LICENSE BLOCK ***** */
  30. define(function(require, exports, module) {
  31. "use strict";
  32. var Range = require("../range").Range;
  33. var FoldLine = require("./fold_line").FoldLine;
  34. var Fold = require("./fold").Fold;
  35. var TokenIterator = require("../token_iterator").TokenIterator;
  36. function Folding() {
  37. /*
  38. * Looks up a fold at a given row/column. Possible values for side:
  39. * -1: ignore a fold if fold.start = row/column
  40. * +1: ignore a fold if fold.end = row/column
  41. */
  42. this.getFoldAt = function(row, column, side) {
  43. var foldLine = this.getFoldLine(row);
  44. if (!foldLine)
  45. return null;
  46. var folds = foldLine.folds;
  47. for (var i = 0; i < folds.length; i++) {
  48. var fold = folds[i];
  49. if (fold.range.contains(row, column)) {
  50. if (side == 1 && fold.range.isEnd(row, column)) {
  51. continue;
  52. } else if (side == -1 && fold.range.isStart(row, column)) {
  53. continue;
  54. }
  55. return fold;
  56. }
  57. }
  58. };
  59. /*
  60. * Returns all folds in the given range. Note, that this will return folds
  61. *
  62. */
  63. this.getFoldsInRange = function(range) {
  64. var start = range.start;
  65. var end = range.end;
  66. var foldLines = this.$foldData;
  67. var foundFolds = [];
  68. start.column += 1;
  69. end.column -= 1;
  70. for (var i = 0; i < foldLines.length; i++) {
  71. var cmp = foldLines[i].range.compareRange(range);
  72. if (cmp == 2) {
  73. // Range is before foldLine. No intersection. This means,
  74. // there might be other foldLines that intersect.
  75. continue;
  76. }
  77. else if (cmp == -2) {
  78. // Range is after foldLine. There can't be any other foldLines then,
  79. // so let's give up.
  80. break;
  81. }
  82. var folds = foldLines[i].folds;
  83. for (var j = 0; j < folds.length; j++) {
  84. var fold = folds[j];
  85. cmp = fold.range.compareRange(range);
  86. if (cmp == -2) {
  87. break;
  88. } else if (cmp == 2) {
  89. continue;
  90. } else
  91. // WTF-state: Can happen due to -1/+1 to start/end column.
  92. if (cmp == 42) {
  93. break;
  94. }
  95. foundFolds.push(fold);
  96. }
  97. }
  98. start.column -= 1;
  99. end.column += 1;
  100. return foundFolds;
  101. };
  102. this.getFoldsInRangeList = function(ranges) {
  103. if (Array.isArray(ranges)) {
  104. var folds = [];
  105. ranges.forEach(function(range) {
  106. folds = folds.concat(this.getFoldsInRange(range));
  107. }, this);
  108. } else {
  109. var folds = this.getFoldsInRange(ranges);
  110. }
  111. return folds;
  112. };
  113. /*
  114. * Returns all folds in the document
  115. */
  116. this.getAllFolds = function() {
  117. var folds = [];
  118. var foldLines = this.$foldData;
  119. for (var i = 0; i < foldLines.length; i++)
  120. for (var j = 0; j < foldLines[i].folds.length; j++)
  121. folds.push(foldLines[i].folds[j]);
  122. return folds;
  123. };
  124. /*
  125. * Returns the string between folds at the given position.
  126. * E.g.
  127. * foo<fold>b|ar<fold>wolrd -> "bar"
  128. * foo<fold>bar<fold>wol|rd -> "world"
  129. * foo<fold>bar<fo|ld>wolrd -> <null>
  130. *
  131. * where | means the position of row/column
  132. *
  133. * The trim option determs if the return string should be trimed according
  134. * to the "side" passed with the trim value:
  135. *
  136. * E.g.
  137. * foo<fold>b|ar<fold>wolrd -trim=-1> "b"
  138. * foo<fold>bar<fold>wol|rd -trim=+1> "rld"
  139. * fo|o<fold>bar<fold>wolrd -trim=00> "foo"
  140. */
  141. this.getFoldStringAt = function(row, column, trim, foldLine) {
  142. foldLine = foldLine || this.getFoldLine(row);
  143. if (!foldLine)
  144. return null;
  145. var lastFold = {
  146. end: { column: 0 }
  147. };
  148. // TODO: Refactor to use getNextFoldTo function.
  149. var str, fold;
  150. for (var i = 0; i < foldLine.folds.length; i++) {
  151. fold = foldLine.folds[i];
  152. var cmp = fold.range.compareEnd(row, column);
  153. if (cmp == -1) {
  154. str = this
  155. .getLine(fold.start.row)
  156. .substring(lastFold.end.column, fold.start.column);
  157. break;
  158. }
  159. else if (cmp === 0) {
  160. return null;
  161. }
  162. lastFold = fold;
  163. }
  164. if (!str)
  165. str = this.getLine(fold.start.row).substring(lastFold.end.column);
  166. if (trim == -1)
  167. return str.substring(0, column - lastFold.end.column);
  168. else if (trim == 1)
  169. return str.substring(column - lastFold.end.column);
  170. else
  171. return str;
  172. };
  173. this.getFoldLine = function(docRow, startFoldLine) {
  174. var foldData = this.$foldData;
  175. var i = 0;
  176. if (startFoldLine)
  177. i = foldData.indexOf(startFoldLine);
  178. if (i == -1)
  179. i = 0;
  180. for (i; i < foldData.length; i++) {
  181. var foldLine = foldData[i];
  182. if (foldLine.start.row <= docRow && foldLine.end.row >= docRow) {
  183. return foldLine;
  184. } else if (foldLine.end.row > docRow) {
  185. return null;
  186. }
  187. }
  188. return null;
  189. };
  190. // returns the fold which starts after or contains docRow
  191. this.getNextFoldLine = function(docRow, startFoldLine) {
  192. var foldData = this.$foldData;
  193. var i = 0;
  194. if (startFoldLine)
  195. i = foldData.indexOf(startFoldLine);
  196. if (i == -1)
  197. i = 0;
  198. for (i; i < foldData.length; i++) {
  199. var foldLine = foldData[i];
  200. if (foldLine.end.row >= docRow) {
  201. return foldLine;
  202. }
  203. }
  204. return null;
  205. };
  206. this.getFoldedRowCount = function(first, last) {
  207. var foldData = this.$foldData, rowCount = last-first+1;
  208. for (var i = 0; i < foldData.length; i++) {
  209. var foldLine = foldData[i],
  210. end = foldLine.end.row,
  211. start = foldLine.start.row;
  212. if (end >= last) {
  213. if (start < last) {
  214. if (start >= first)
  215. rowCount -= last-start;
  216. else
  217. rowCount = 0; // in one fold
  218. }
  219. break;
  220. } else if (end >= first){
  221. if (start >= first) // fold inside range
  222. rowCount -= end-start;
  223. else
  224. rowCount -= end-first+1;
  225. }
  226. }
  227. return rowCount;
  228. };
  229. this.$addFoldLine = function(foldLine) {
  230. this.$foldData.push(foldLine);
  231. this.$foldData.sort(function(a, b) {
  232. return a.start.row - b.start.row;
  233. });
  234. return foldLine;
  235. };
  236. /**
  237. * Adds a new fold.
  238. *
  239. * @returns
  240. * The new created Fold object or an existing fold object in case the
  241. * passed in range fits an existing fold exactly.
  242. */
  243. this.addFold = function(placeholder, range) {
  244. var foldData = this.$foldData;
  245. var added = false;
  246. var fold;
  247. if (placeholder instanceof Fold)
  248. fold = placeholder;
  249. else {
  250. fold = new Fold(range, placeholder);
  251. fold.collapseChildren = range.collapseChildren;
  252. }
  253. this.$clipRangeToDocument(fold.range);
  254. var startRow = fold.start.row;
  255. var startColumn = fold.start.column;
  256. var endRow = fold.end.row;
  257. var endColumn = fold.end.column;
  258. // --- Some checking ---
  259. if (!(startRow < endRow ||
  260. startRow == endRow && startColumn <= endColumn - 2))
  261. throw new Error("The range has to be at least 2 characters width");
  262. var startFold = this.getFoldAt(startRow, startColumn, 1);
  263. var endFold = this.getFoldAt(endRow, endColumn, -1);
  264. if (startFold && endFold == startFold)
  265. return startFold.addSubFold(fold);
  266. if (startFold && !startFold.range.isStart(startRow, startColumn))
  267. this.removeFold(startFold);
  268. if (endFold && !endFold.range.isEnd(endRow, endColumn))
  269. this.removeFold(endFold);
  270. // Check if there are folds in the range we create the new fold for.
  271. var folds = this.getFoldsInRange(fold.range);
  272. if (folds.length > 0) {
  273. // Remove the folds from fold data.
  274. this.removeFolds(folds);
  275. // Add the removed folds as subfolds on the new fold.
  276. folds.forEach(function(subFold) {
  277. fold.addSubFold(subFold);
  278. });
  279. }
  280. for (var i = 0; i < foldData.length; i++) {
  281. var foldLine = foldData[i];
  282. if (endRow == foldLine.start.row) {
  283. foldLine.addFold(fold);
  284. added = true;
  285. break;
  286. } else if (startRow == foldLine.end.row) {
  287. foldLine.addFold(fold);
  288. added = true;
  289. if (!fold.sameRow) {
  290. // Check if we might have to merge two FoldLines.
  291. var foldLineNext = foldData[i + 1];
  292. if (foldLineNext && foldLineNext.start.row == endRow) {
  293. // We need to merge!
  294. foldLine.merge(foldLineNext);
  295. break;
  296. }
  297. }
  298. break;
  299. } else if (endRow <= foldLine.start.row) {
  300. break;
  301. }
  302. }
  303. if (!added)
  304. foldLine = this.$addFoldLine(new FoldLine(this.$foldData, fold));
  305. if (this.$useWrapMode)
  306. this.$updateWrapData(foldLine.start.row, foldLine.start.row);
  307. else
  308. this.$updateRowLengthCache(foldLine.start.row, foldLine.start.row);
  309. // Notify that fold data has changed.
  310. this.$modified = true;
  311. this._signal("changeFold", { data: fold, action: "add" });
  312. return fold;
  313. };
  314. this.addFolds = function(folds) {
  315. folds.forEach(function(fold) {
  316. this.addFold(fold);
  317. }, this);
  318. };
  319. this.removeFold = function(fold) {
  320. var foldLine = fold.foldLine;
  321. var startRow = foldLine.start.row;
  322. var endRow = foldLine.end.row;
  323. var foldLines = this.$foldData;
  324. var folds = foldLine.folds;
  325. // Simple case where there is only one fold in the FoldLine such that
  326. // the entire fold line can get removed directly.
  327. if (folds.length == 1) {
  328. foldLines.splice(foldLines.indexOf(foldLine), 1);
  329. } else
  330. // If the fold is the last fold of the foldLine, just remove it.
  331. if (foldLine.range.isEnd(fold.end.row, fold.end.column)) {
  332. folds.pop();
  333. foldLine.end.row = folds[folds.length - 1].end.row;
  334. foldLine.end.column = folds[folds.length - 1].end.column;
  335. } else
  336. // If the fold is the first fold of the foldLine, just remove it.
  337. if (foldLine.range.isStart(fold.start.row, fold.start.column)) {
  338. folds.shift();
  339. foldLine.start.row = folds[0].start.row;
  340. foldLine.start.column = folds[0].start.column;
  341. } else
  342. // We know there are more then 2 folds and the fold is not at the edge.
  343. // This means, the fold is somewhere in between.
  344. //
  345. // If the fold is in one row, we just can remove it.
  346. if (fold.sameRow) {
  347. folds.splice(folds.indexOf(fold), 1);
  348. } else
  349. // The fold goes over more then one row. This means remvoing this fold
  350. // will cause the fold line to get splitted up. newFoldLine is the second part
  351. {
  352. var newFoldLine = foldLine.split(fold.start.row, fold.start.column);
  353. folds = newFoldLine.folds;
  354. folds.shift();
  355. newFoldLine.start.row = folds[0].start.row;
  356. newFoldLine.start.column = folds[0].start.column;
  357. }
  358. if (!this.$updating) {
  359. if (this.$useWrapMode)
  360. this.$updateWrapData(startRow, endRow);
  361. else
  362. this.$updateRowLengthCache(startRow, endRow);
  363. }
  364. // Notify that fold data has changed.
  365. this.$modified = true;
  366. this._signal("changeFold", { data: fold, action: "remove" });
  367. };
  368. this.removeFolds = function(folds) {
  369. // We need to clone the folds array passed in as it might be the folds
  370. // array of a fold line and as we call this.removeFold(fold), folds
  371. // are removed from folds and changes the current index.
  372. var cloneFolds = [];
  373. for (var i = 0; i < folds.length; i++) {
  374. cloneFolds.push(folds[i]);
  375. }
  376. cloneFolds.forEach(function(fold) {
  377. this.removeFold(fold);
  378. }, this);
  379. this.$modified = true;
  380. };
  381. this.expandFold = function(fold) {
  382. this.removeFold(fold);
  383. fold.subFolds.forEach(function(subFold) {
  384. fold.restoreRange(subFold);
  385. this.addFold(subFold);
  386. }, this);
  387. if (fold.collapseChildren > 0) {
  388. this.foldAll(fold.start.row+1, fold.end.row, fold.collapseChildren-1);
  389. }
  390. fold.subFolds = [];
  391. };
  392. this.expandFolds = function(folds) {
  393. folds.forEach(function(fold) {
  394. this.expandFold(fold);
  395. }, this);
  396. };
  397. this.unfold = function(location, expandInner) {
  398. var range, folds;
  399. if (location == null) {
  400. range = new Range(0, 0, this.getLength(), 0);
  401. expandInner = true;
  402. } else if (typeof location == "number")
  403. range = new Range(location, 0, location, this.getLine(location).length);
  404. else if ("row" in location)
  405. range = Range.fromPoints(location, location);
  406. else
  407. range = location;
  408. folds = this.getFoldsInRangeList(range);
  409. if (expandInner) {
  410. this.removeFolds(folds);
  411. } else {
  412. var subFolds = folds;
  413. // TODO: might be better to remove and add folds in one go instead of using
  414. // expandFolds several times.
  415. while (subFolds.length) {
  416. this.expandFolds(subFolds);
  417. subFolds = this.getFoldsInRangeList(range);
  418. }
  419. }
  420. if (folds.length)
  421. return folds;
  422. };
  423. /*
  424. * Checks if a given documentRow is folded. This is true if there are some
  425. * folded parts such that some parts of the line is still visible.
  426. **/
  427. this.isRowFolded = function(docRow, startFoldRow) {
  428. return !!this.getFoldLine(docRow, startFoldRow);
  429. };
  430. this.getRowFoldEnd = function(docRow, startFoldRow) {
  431. var foldLine = this.getFoldLine(docRow, startFoldRow);
  432. return foldLine ? foldLine.end.row : docRow;
  433. };
  434. this.getRowFoldStart = function(docRow, startFoldRow) {
  435. var foldLine = this.getFoldLine(docRow, startFoldRow);
  436. return foldLine ? foldLine.start.row : docRow;
  437. };
  438. this.getFoldDisplayLine = function(foldLine, endRow, endColumn, startRow, startColumn) {
  439. if (startRow == null)
  440. startRow = foldLine.start.row;
  441. if (startColumn == null)
  442. startColumn = 0;
  443. if (endRow == null)
  444. endRow = foldLine.end.row;
  445. if (endColumn == null)
  446. endColumn = this.getLine(endRow).length;
  447. // Build the textline using the FoldLine walker.
  448. var doc = this.doc;
  449. var textLine = "";
  450. foldLine.walk(function(placeholder, row, column, lastColumn) {
  451. if (row < startRow)
  452. return;
  453. if (row == startRow) {
  454. if (column < startColumn)
  455. return;
  456. lastColumn = Math.max(startColumn, lastColumn);
  457. }
  458. if (placeholder != null) {
  459. textLine += placeholder;
  460. } else {
  461. textLine += doc.getLine(row).substring(lastColumn, column);
  462. }
  463. }, endRow, endColumn);
  464. return textLine;
  465. };
  466. this.getDisplayLine = function(row, endColumn, startRow, startColumn) {
  467. var foldLine = this.getFoldLine(row);
  468. if (!foldLine) {
  469. var line;
  470. line = this.doc.getLine(row);
  471. return line.substring(startColumn || 0, endColumn || line.length);
  472. } else {
  473. return this.getFoldDisplayLine(
  474. foldLine, row, endColumn, startRow, startColumn);
  475. }
  476. };
  477. this.$cloneFoldData = function() {
  478. var fd = [];
  479. fd = this.$foldData.map(function(foldLine) {
  480. var folds = foldLine.folds.map(function(fold) {
  481. return fold.clone();
  482. });
  483. return new FoldLine(fd, folds);
  484. });
  485. return fd;
  486. };
  487. this.toggleFold = function(tryToUnfold) {
  488. var selection = this.selection;
  489. var range = selection.getRange();
  490. var fold;
  491. var bracketPos;
  492. if (range.isEmpty()) {
  493. var cursor = range.start;
  494. fold = this.getFoldAt(cursor.row, cursor.column);
  495. if (fold) {
  496. this.expandFold(fold);
  497. return;
  498. } else if (bracketPos = this.findMatchingBracket(cursor)) {
  499. if (range.comparePoint(bracketPos) == 1) {
  500. range.end = bracketPos;
  501. } else {
  502. range.start = bracketPos;
  503. range.start.column++;
  504. range.end.column--;
  505. }
  506. } else if (bracketPos = this.findMatchingBracket({row: cursor.row, column: cursor.column + 1})) {
  507. if (range.comparePoint(bracketPos) == 1)
  508. range.end = bracketPos;
  509. else
  510. range.start = bracketPos;
  511. range.start.column++;
  512. } else {
  513. range = this.getCommentFoldRange(cursor.row, cursor.column) || range;
  514. }
  515. } else {
  516. var folds = this.getFoldsInRange(range);
  517. if (tryToUnfold && folds.length) {
  518. this.expandFolds(folds);
  519. return;
  520. } else if (folds.length == 1 ) {
  521. fold = folds[0];
  522. }
  523. }
  524. if (!fold)
  525. fold = this.getFoldAt(range.start.row, range.start.column);
  526. if (fold && fold.range.toString() == range.toString()) {
  527. this.expandFold(fold);
  528. return;
  529. }
  530. var placeholder = "...";
  531. if (!range.isMultiLine()) {
  532. placeholder = this.getTextRange(range);
  533. if (placeholder.length < 4)
  534. return;
  535. placeholder = placeholder.trim().substring(0, 2) + "..";
  536. }
  537. this.addFold(placeholder, range);
  538. };
  539. this.getCommentFoldRange = function(row, column, dir) {
  540. var iterator = new TokenIterator(this, row, column);
  541. var token = iterator.getCurrentToken();
  542. if (token && /^comment|string/.test(token.type)) {
  543. var range = new Range();
  544. var re = new RegExp(token.type.replace(/\..*/, "\\."));
  545. if (dir != 1) {
  546. do {
  547. token = iterator.stepBackward();
  548. } while (token && re.test(token.type));
  549. iterator.stepForward();
  550. }
  551. range.start.row = iterator.getCurrentTokenRow();
  552. range.start.column = iterator.getCurrentTokenColumn() + 2;
  553. iterator = new TokenIterator(this, row, column);
  554. if (dir != -1) {
  555. do {
  556. token = iterator.stepForward();
  557. } while (token && re.test(token.type));
  558. token = iterator.stepBackward();
  559. } else
  560. token = iterator.getCurrentToken();
  561. range.end.row = iterator.getCurrentTokenRow();
  562. range.end.column = iterator.getCurrentTokenColumn() + token.value.length - 2;
  563. return range;
  564. }
  565. };
  566. this.foldAll = function(startRow, endRow, depth) {
  567. if (depth == undefined)
  568. depth = 100000; // JSON.stringify doesn't hanle Infinity
  569. var foldWidgets = this.foldWidgets;
  570. if (!foldWidgets)
  571. return; // mode doesn't support folding
  572. endRow = endRow || this.getLength();
  573. startRow = startRow || 0;
  574. for (var row = startRow; row < endRow; row++) {
  575. if (foldWidgets[row] == null)
  576. foldWidgets[row] = this.getFoldWidget(row);
  577. if (foldWidgets[row] != "start")
  578. continue;
  579. var range = this.getFoldWidgetRange(row);
  580. // sometimes range can be incompatible with existing fold
  581. // TODO change addFold to return null istead of throwing
  582. if (range && range.isMultiLine()
  583. && range.end.row <= endRow
  584. && range.start.row >= startRow
  585. ) {
  586. row = range.end.row;
  587. try {
  588. // addFold can change the range
  589. var fold = this.addFold("...", range);
  590. if (fold)
  591. fold.collapseChildren = depth;
  592. } catch(e) {}
  593. }
  594. }
  595. };
  596. // structured folding
  597. this.$foldStyles = {
  598. "manual": 1,
  599. "markbegin": 1,
  600. "markbeginend": 1
  601. };
  602. this.$foldStyle = "markbegin";
  603. this.setFoldStyle = function(style) {
  604. if (!this.$foldStyles[style])
  605. throw new Error("invalid fold style: " + style + "[" + Object.keys(this.$foldStyles).join(", ") + "]");
  606. if (this.$foldStyle == style)
  607. return;
  608. this.$foldStyle = style;
  609. if (style == "manual")
  610. this.unfold();
  611. // reset folding
  612. var mode = this.$foldMode;
  613. this.$setFolding(null);
  614. this.$setFolding(mode);
  615. };
  616. this.$setFolding = function(foldMode) {
  617. if (this.$foldMode == foldMode)
  618. return;
  619. this.$foldMode = foldMode;
  620. this.off('change', this.$updateFoldWidgets);
  621. this.off('tokenizerUpdate', this.$tokenizerUpdateFoldWidgets);
  622. this._signal("changeAnnotation");
  623. if (!foldMode || this.$foldStyle == "manual") {
  624. this.foldWidgets = null;
  625. return;
  626. }
  627. this.foldWidgets = [];
  628. this.getFoldWidget = foldMode.getFoldWidget.bind(foldMode, this, this.$foldStyle);
  629. this.getFoldWidgetRange = foldMode.getFoldWidgetRange.bind(foldMode, this, this.$foldStyle);
  630. this.$updateFoldWidgets = this.updateFoldWidgets.bind(this);
  631. this.$tokenizerUpdateFoldWidgets = this.tokenizerUpdateFoldWidgets.bind(this);
  632. this.on('change', this.$updateFoldWidgets);
  633. this.on('tokenizerUpdate', this.$tokenizerUpdateFoldWidgets);
  634. };
  635. this.getParentFoldRangeData = function (row, ignoreCurrent) {
  636. var fw = this.foldWidgets;
  637. if (!fw || (ignoreCurrent && fw[row]))
  638. return {};
  639. var i = row - 1, firstRange;
  640. while (i >= 0) {
  641. var c = fw[i];
  642. if (c == null)
  643. c = fw[i] = this.getFoldWidget(i);
  644. if (c == "start") {
  645. var range = this.getFoldWidgetRange(i);
  646. if (!firstRange)
  647. firstRange = range;
  648. if (range && range.end.row >= row)
  649. break;
  650. }
  651. i--;
  652. }
  653. return {
  654. range: i !== -1 && range,
  655. firstRange: firstRange
  656. };
  657. };
  658. this.onFoldWidgetClick = function(row, e) {
  659. e = e.domEvent;
  660. var options = {
  661. children: e.shiftKey,
  662. all: e.ctrlKey || e.metaKey,
  663. siblings: e.altKey
  664. };
  665. var range = this.$toggleFoldWidget(row, options);
  666. if (!range) {
  667. var el = (e.target || e.srcElement);
  668. if (el && /ace_fold-widget/.test(el.className))
  669. el.className += " ace_invalid";
  670. }
  671. };
  672. this.$toggleFoldWidget = function(row, options) {
  673. if (!this.getFoldWidget)
  674. return;
  675. var type = this.getFoldWidget(row);
  676. var line = this.getLine(row);
  677. var dir = type === "end" ? -1 : 1;
  678. var fold = this.getFoldAt(row, dir === -1 ? 0 : line.length, dir);
  679. if (fold) {
  680. if (options.children || options.all)
  681. this.removeFold(fold);
  682. else
  683. this.expandFold(fold);
  684. return;
  685. }
  686. var range = this.getFoldWidgetRange(row, true);
  687. // sometimes singleline folds can be missed by the code above
  688. if (range && !range.isMultiLine()) {
  689. fold = this.getFoldAt(range.start.row, range.start.column, 1);
  690. if (fold && range.isEqual(fold.range)) {
  691. this.removeFold(fold);
  692. return;
  693. }
  694. }
  695. if (options.siblings) {
  696. var data = this.getParentFoldRangeData(row);
  697. if (data.range) {
  698. var startRow = data.range.start.row + 1;
  699. var endRow = data.range.end.row;
  700. }
  701. this.foldAll(startRow, endRow, options.all ? 10000 : 0);
  702. } else if (options.children) {
  703. endRow = range ? range.end.row : this.getLength();
  704. this.foldAll(row + 1, endRow, options.all ? 10000 : 0);
  705. } else if (range) {
  706. if (options.all)
  707. range.collapseChildren = 10000;
  708. this.addFold("...", range);
  709. }
  710. return range;
  711. };
  712. this.toggleFoldWidget = function(toggleParent) {
  713. var row = this.selection.getCursor().row;
  714. row = this.getRowFoldStart(row);
  715. var range = this.$toggleFoldWidget(row, {});
  716. if (range)
  717. return;
  718. // handle toggleParent
  719. var data = this.getParentFoldRangeData(row, true);
  720. range = data.range || data.firstRange;
  721. if (range) {
  722. row = range.start.row;
  723. var fold = this.getFoldAt(row, this.getLine(row).length, 1);
  724. if (fold) {
  725. this.removeFold(fold);
  726. } else {
  727. this.addFold("...", range);
  728. }
  729. }
  730. };
  731. this.updateFoldWidgets = function(delta) {
  732. var firstRow = delta.start.row;
  733. var len = delta.end.row - firstRow;
  734. if (len === 0) {
  735. this.foldWidgets[firstRow] = null;
  736. } else if (delta.action == 'remove') {
  737. this.foldWidgets.splice(firstRow, len + 1, null);
  738. } else {
  739. var args = Array(len + 1);
  740. args.unshift(firstRow, 1);
  741. this.foldWidgets.splice.apply(this.foldWidgets, args);
  742. }
  743. };
  744. this.tokenizerUpdateFoldWidgets = function(e) {
  745. var rows = e.data;
  746. if (rows.first != rows.last) {
  747. if (this.foldWidgets.length > rows.first)
  748. this.foldWidgets.splice(rows.first, this.foldWidgets.length);
  749. }
  750. };
  751. }
  752. exports.Folding = Folding;
  753. });