folding.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846
  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 (
  267. (startFold && !startFold.range.isStart(startRow, startColumn))
  268. || (endFold && !endFold.range.isEnd(endRow, endColumn))
  269. ) {
  270. throw new Error("A fold can't intersect already existing fold" + fold.range + startFold.range);
  271. }
  272. // Check if there are folds in the range we create the new fold for.
  273. var folds = this.getFoldsInRange(fold.range);
  274. if (folds.length > 0) {
  275. // Remove the folds from fold data.
  276. this.removeFolds(folds);
  277. // Add the removed folds as subfolds on the new fold.
  278. folds.forEach(function(subFold) {
  279. fold.addSubFold(subFold);
  280. });
  281. }
  282. for (var i = 0; i < foldData.length; i++) {
  283. var foldLine = foldData[i];
  284. if (endRow == foldLine.start.row) {
  285. foldLine.addFold(fold);
  286. added = true;
  287. break;
  288. } else if (startRow == foldLine.end.row) {
  289. foldLine.addFold(fold);
  290. added = true;
  291. if (!fold.sameRow) {
  292. // Check if we might have to merge two FoldLines.
  293. var foldLineNext = foldData[i + 1];
  294. if (foldLineNext && foldLineNext.start.row == endRow) {
  295. // We need to merge!
  296. foldLine.merge(foldLineNext);
  297. break;
  298. }
  299. }
  300. break;
  301. } else if (endRow <= foldLine.start.row) {
  302. break;
  303. }
  304. }
  305. if (!added)
  306. foldLine = this.$addFoldLine(new FoldLine(this.$foldData, fold));
  307. if (this.$useWrapMode)
  308. this.$updateWrapData(foldLine.start.row, foldLine.start.row);
  309. else
  310. this.$updateRowLengthCache(foldLine.start.row, foldLine.start.row);
  311. // Notify that fold data has changed.
  312. this.$modified = true;
  313. this._emit("changeFold", { data: fold, action: "add" });
  314. return fold;
  315. };
  316. this.addFolds = function(folds) {
  317. folds.forEach(function(fold) {
  318. this.addFold(fold);
  319. }, this);
  320. };
  321. this.removeFold = function(fold) {
  322. var foldLine = fold.foldLine;
  323. var startRow = foldLine.start.row;
  324. var endRow = foldLine.end.row;
  325. var foldLines = this.$foldData;
  326. var folds = foldLine.folds;
  327. // Simple case where there is only one fold in the FoldLine such that
  328. // the entire fold line can get removed directly.
  329. if (folds.length == 1) {
  330. foldLines.splice(foldLines.indexOf(foldLine), 1);
  331. } else
  332. // If the fold is the last fold of the foldLine, just remove it.
  333. if (foldLine.range.isEnd(fold.end.row, fold.end.column)) {
  334. folds.pop();
  335. foldLine.end.row = folds[folds.length - 1].end.row;
  336. foldLine.end.column = folds[folds.length - 1].end.column;
  337. } else
  338. // If the fold is the first fold of the foldLine, just remove it.
  339. if (foldLine.range.isStart(fold.start.row, fold.start.column)) {
  340. folds.shift();
  341. foldLine.start.row = folds[0].start.row;
  342. foldLine.start.column = folds[0].start.column;
  343. } else
  344. // We know there are more then 2 folds and the fold is not at the edge.
  345. // This means, the fold is somewhere in between.
  346. //
  347. // If the fold is in one row, we just can remove it.
  348. if (fold.sameRow) {
  349. folds.splice(folds.indexOf(fold), 1);
  350. } else
  351. // The fold goes over more then one row. This means remvoing this fold
  352. // will cause the fold line to get splitted up. newFoldLine is the second part
  353. {
  354. var newFoldLine = foldLine.split(fold.start.row, fold.start.column);
  355. folds = newFoldLine.folds;
  356. folds.shift();
  357. newFoldLine.start.row = folds[0].start.row;
  358. newFoldLine.start.column = folds[0].start.column;
  359. }
  360. if (!this.$updating) {
  361. if (this.$useWrapMode)
  362. this.$updateWrapData(startRow, endRow);
  363. else
  364. this.$updateRowLengthCache(startRow, endRow);
  365. }
  366. // Notify that fold data has changed.
  367. this.$modified = true;
  368. this._emit("changeFold", { data: fold, action: "remove" });
  369. };
  370. this.removeFolds = function(folds) {
  371. // We need to clone the folds array passed in as it might be the folds
  372. // array of a fold line and as we call this.removeFold(fold), folds
  373. // are removed from folds and changes the current index.
  374. var cloneFolds = [];
  375. for (var i = 0; i < folds.length; i++) {
  376. cloneFolds.push(folds[i]);
  377. }
  378. cloneFolds.forEach(function(fold) {
  379. this.removeFold(fold);
  380. }, this);
  381. this.$modified = true;
  382. };
  383. this.expandFold = function(fold) {
  384. this.removeFold(fold);
  385. fold.subFolds.forEach(function(subFold) {
  386. fold.restoreRange(subFold);
  387. this.addFold(subFold);
  388. }, this);
  389. if (fold.collapseChildren > 0) {
  390. this.foldAll(fold.start.row+1, fold.end.row, fold.collapseChildren-1);
  391. }
  392. fold.subFolds = [];
  393. };
  394. this.expandFolds = function(folds) {
  395. folds.forEach(function(fold) {
  396. this.expandFold(fold);
  397. }, this);
  398. };
  399. this.unfold = function(location, expandInner) {
  400. var range, folds;
  401. if (location == null) {
  402. range = new Range(0, 0, this.getLength(), 0);
  403. expandInner = true;
  404. } else if (typeof location == "number")
  405. range = new Range(location, 0, location, this.getLine(location).length);
  406. else if ("row" in location)
  407. range = Range.fromPoints(location, location);
  408. else
  409. range = location;
  410. folds = this.getFoldsInRangeList(range);
  411. if (expandInner) {
  412. this.removeFolds(folds);
  413. } else {
  414. // TODO: might need to remove and add folds in one go instead of using
  415. // expandFolds several times.
  416. while (folds.length) {
  417. this.expandFolds(folds);
  418. folds = this.getFoldsInRangeList(range);
  419. }
  420. }
  421. };
  422. /*
  423. * Checks if a given documentRow is folded. This is true if there are some
  424. * folded parts such that some parts of the line is still visible.
  425. **/
  426. this.isRowFolded = function(docRow, startFoldRow) {
  427. return !!this.getFoldLine(docRow, startFoldRow);
  428. };
  429. this.getRowFoldEnd = function(docRow, startFoldRow) {
  430. var foldLine = this.getFoldLine(docRow, startFoldRow);
  431. return foldLine ? foldLine.end.row : docRow;
  432. };
  433. this.getRowFoldStart = function(docRow, startFoldRow) {
  434. var foldLine = this.getFoldLine(docRow, startFoldRow);
  435. return foldLine ? foldLine.start.row : docRow;
  436. };
  437. this.getFoldDisplayLine = function(foldLine, endRow, endColumn, startRow, startColumn) {
  438. if (startRow == null) {
  439. startRow = foldLine.start.row;
  440. startColumn = 0;
  441. }
  442. if (endRow == null) {
  443. endRow = foldLine.end.row;
  444. endColumn = this.getLine(endRow).length;
  445. }
  446. // Build the textline using the FoldLine walker.
  447. var doc = this.doc;
  448. var textLine = "";
  449. foldLine.walk(function(placeholder, row, column, lastColumn) {
  450. if (row < startRow)
  451. return;
  452. if (row == startRow) {
  453. if (column < startColumn)
  454. return;
  455. lastColumn = Math.max(startColumn, lastColumn);
  456. }
  457. if (placeholder != null) {
  458. textLine += placeholder;
  459. } else {
  460. textLine += doc.getLine(row).substring(lastColumn, column);
  461. }
  462. }, endRow, endColumn);
  463. return textLine;
  464. };
  465. this.getDisplayLine = function(row, endColumn, startRow, startColumn) {
  466. var foldLine = this.getFoldLine(row);
  467. if (!foldLine) {
  468. var line;
  469. line = this.doc.getLine(row);
  470. return line.substring(startColumn || 0, endColumn || line.length);
  471. } else {
  472. return this.getFoldDisplayLine(
  473. foldLine, row, endColumn, startRow, startColumn);
  474. }
  475. };
  476. this.$cloneFoldData = function() {
  477. var fd = [];
  478. fd = this.$foldData.map(function(foldLine) {
  479. var folds = foldLine.folds.map(function(fold) {
  480. return fold.clone();
  481. });
  482. return new FoldLine(fd, folds);
  483. });
  484. return fd;
  485. };
  486. this.toggleFold = function(tryToUnfold) {
  487. var selection = this.selection;
  488. var range = selection.getRange();
  489. var fold;
  490. var bracketPos;
  491. if (range.isEmpty()) {
  492. var cursor = range.start;
  493. fold = this.getFoldAt(cursor.row, cursor.column);
  494. if (fold) {
  495. this.expandFold(fold);
  496. return;
  497. } else if (bracketPos = this.findMatchingBracket(cursor)) {
  498. if (range.comparePoint(bracketPos) == 1) {
  499. range.end = bracketPos;
  500. } else {
  501. range.start = bracketPos;
  502. range.start.column++;
  503. range.end.column--;
  504. }
  505. } else if (bracketPos = this.findMatchingBracket({row: cursor.row, column: cursor.column + 1})) {
  506. if (range.comparePoint(bracketPos) == 1)
  507. range.end = bracketPos;
  508. else
  509. range.start = bracketPos;
  510. range.start.column++;
  511. } else {
  512. range = this.getCommentFoldRange(cursor.row, cursor.column) || range;
  513. }
  514. } else {
  515. var folds = this.getFoldsInRange(range);
  516. if (tryToUnfold && folds.length) {
  517. this.expandFolds(folds);
  518. return;
  519. } else if (folds.length == 1 ) {
  520. fold = folds[0];
  521. }
  522. }
  523. if (!fold)
  524. fold = this.getFoldAt(range.start.row, range.start.column);
  525. if (fold && fold.range.toString() == range.toString()) {
  526. this.expandFold(fold);
  527. return;
  528. }
  529. var placeholder = "...";
  530. if (!range.isMultiLine()) {
  531. placeholder = this.getTextRange(range);
  532. if(placeholder.length < 4)
  533. return;
  534. placeholder = placeholder.trim().substring(0, 2) + "..";
  535. }
  536. this.addFold(placeholder, range);
  537. };
  538. this.getCommentFoldRange = function(row, column, dir) {
  539. var iterator = new TokenIterator(this, row, column);
  540. var token = iterator.getCurrentToken();
  541. if (token && /^comment|string/.test(token.type)) {
  542. var range = new Range();
  543. var re = new RegExp(token.type.replace(/\..*/, "\\."));
  544. if (dir != 1) {
  545. do {
  546. token = iterator.stepBackward();
  547. } while(token && re.test(token.type));
  548. iterator.stepForward();
  549. }
  550. range.start.row = iterator.getCurrentTokenRow();
  551. range.start.column = iterator.getCurrentTokenColumn() + 2;
  552. iterator = new TokenIterator(this, row, column);
  553. if (dir != -1) {
  554. do {
  555. token = iterator.stepForward();
  556. } while(token && re.test(token.type));
  557. token = iterator.stepBackward();
  558. } else
  559. token = iterator.getCurrentToken();
  560. range.end.row = iterator.getCurrentTokenRow();
  561. range.end.column = iterator.getCurrentTokenColumn() + token.value.length - 2;
  562. return range;
  563. }
  564. };
  565. this.foldAll = function(startRow, endRow, depth) {
  566. if (depth == undefined)
  567. depth = 100000; // JSON.stringify doesn't hanle Infinity
  568. var foldWidgets = this.foldWidgets;
  569. if (!foldWidgets)
  570. return; // mode doesn't support folding
  571. endRow = endRow || this.getLength();
  572. startRow = startRow || 0;
  573. for (var row = startRow; row < endRow; row++) {
  574. if (foldWidgets[row] == null)
  575. foldWidgets[row] = this.getFoldWidget(row);
  576. if (foldWidgets[row] != "start")
  577. continue;
  578. var range = this.getFoldWidgetRange(row);
  579. // sometimes range can be incompatible with existing fold
  580. // TODO change addFold to return null istead of throwing
  581. if (range && range.isMultiLine()
  582. && range.end.row <= endRow
  583. && range.start.row >= startRow
  584. ) {
  585. row = range.end.row;
  586. try {
  587. // addFold can change the range
  588. var fold = this.addFold("...", range);
  589. if (fold)
  590. fold.collapseChildren = depth;
  591. } catch(e) {}
  592. }
  593. }
  594. };
  595. // structured folding
  596. this.$foldStyles = {
  597. "manual": 1,
  598. "markbegin": 1,
  599. "markbeginend": 1
  600. };
  601. this.$foldStyle = "markbegin";
  602. this.setFoldStyle = function(style) {
  603. if (!this.$foldStyles[style])
  604. throw new Error("invalid fold style: " + style + "[" + Object.keys(this.$foldStyles).join(", ") + "]");
  605. if (this.$foldStyle == style)
  606. return;
  607. this.$foldStyle = style;
  608. if (style == "manual")
  609. this.unfold();
  610. // reset folding
  611. var mode = this.$foldMode;
  612. this.$setFolding(null);
  613. this.$setFolding(mode);
  614. };
  615. this.$setFolding = function(foldMode) {
  616. if (this.$foldMode == foldMode)
  617. return;
  618. this.$foldMode = foldMode;
  619. this.removeListener('change', this.$updateFoldWidgets);
  620. this._emit("changeAnnotation");
  621. if (!foldMode || this.$foldStyle == "manual") {
  622. this.foldWidgets = null;
  623. return;
  624. }
  625. this.foldWidgets = [];
  626. this.getFoldWidget = foldMode.getFoldWidget.bind(foldMode, this, this.$foldStyle);
  627. this.getFoldWidgetRange = foldMode.getFoldWidgetRange.bind(foldMode, this, this.$foldStyle);
  628. this.$updateFoldWidgets = this.updateFoldWidgets.bind(this);
  629. this.on('change', this.$updateFoldWidgets);
  630. };
  631. this.getParentFoldRangeData = function (row, ignoreCurrent) {
  632. var fw = this.foldWidgets;
  633. if (!fw || (ignoreCurrent && fw[row]))
  634. return {};
  635. var i = row - 1, firstRange;
  636. while (i >= 0) {
  637. var c = fw[i];
  638. if (c == null)
  639. c = fw[i] = this.getFoldWidget(i);
  640. if (c == "start") {
  641. var range = this.getFoldWidgetRange(i);
  642. if (!firstRange)
  643. firstRange = range;
  644. if (range && range.end.row >= row)
  645. break;
  646. }
  647. i--;
  648. }
  649. return {
  650. range: i !== -1 && range,
  651. firstRange: firstRange
  652. };
  653. }
  654. this.onFoldWidgetClick = function(row, e) {
  655. e = e.domEvent;
  656. var options = {
  657. children: e.shiftKey,
  658. all: e.ctrlKey || e.metaKey,
  659. siblings: e.altKey
  660. };
  661. var range = this.$toggleFoldWidget(row, options);
  662. if (!range)
  663. (e.target || e.srcElement).className += " ace_invalid";
  664. };
  665. this.$toggleFoldWidget = function(row, options) {
  666. if (!this.getFoldWidget)
  667. return;
  668. var type = this.getFoldWidget(row);
  669. var line = this.getLine(row);
  670. var dir = type === "end" ? -1 : 1;
  671. var fold = this.getFoldAt(row, dir === -1 ? 0 : line.length, dir);
  672. if (fold) {
  673. if (options.children || options.all)
  674. this.removeFold(fold);
  675. else
  676. this.expandFold(fold);
  677. return;
  678. }
  679. var range = this.getFoldWidgetRange(row, true);
  680. // sometimes singleline folds can be missed by the code above
  681. if (range && !range.isMultiLine()) {
  682. fold = this.getFoldAt(range.start.row, range.start.column, 1);
  683. if (fold && range.isEqual(fold.range)) {
  684. this.removeFold(fold);
  685. return;
  686. }
  687. }
  688. if (options.siblings) {
  689. var data = this.getParentFoldRangeData(row);
  690. if (data.range) {
  691. var startRow = data.range.start.row + 1;
  692. var endRow = data.range.end.row;
  693. }
  694. this.foldAll(startRow, endRow, options.all ? 10000 : 0);
  695. } else if (options.children) {
  696. endRow = range ? range.end.row : this.getLength();
  697. this.foldAll(row + 1, range.end.row, options.all ? 10000 : 0);
  698. } else if (range) {
  699. if (options.all)
  700. range.collapseChildren = 10000;
  701. this.addFold("...", range);
  702. }
  703. return range;
  704. };
  705. this.toggleFoldWidget = function(toggleParent) {
  706. var row = this.selection.getCursor().row;
  707. row = this.getRowFoldStart(row);
  708. var range = this.$toggleFoldWidget(row, {});
  709. if (range)
  710. return;
  711. // handle toggleParent
  712. var data = this.getParentFoldRangeData(row, true);
  713. range = data.range || data.firstRange;
  714. if (range) {
  715. row = range.start.row;
  716. var fold = this.getFoldAt(row, this.getLine(row).length, 1);
  717. if (fold) {
  718. this.removeFold(fold);
  719. } else {
  720. this.addFold("...", range);
  721. }
  722. }
  723. };
  724. this.updateFoldWidgets = function(e) {
  725. var delta = e.data;
  726. var range = delta.range;
  727. var firstRow = range.start.row;
  728. var len = range.end.row - firstRow;
  729. if (len === 0) {
  730. this.foldWidgets[firstRow] = null;
  731. } else if (delta.action == "removeText" || delta.action == "removeLines") {
  732. this.foldWidgets.splice(firstRow, len + 1, null);
  733. } else {
  734. var args = Array(len + 1);
  735. args.unshift(firstRow, 1);
  736. this.foldWidgets.splice.apply(this.foldWidgets, args);
  737. }
  738. };
  739. }
  740. exports.Folding = Folding;
  741. });