foldingModel.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. import { Emitter } from '../../../base/common/event.js';
  6. import { FoldingRegions } from './foldingRanges.js';
  7. export class FoldingModel {
  8. constructor(textModel, decorationProvider) {
  9. this._updateEventEmitter = new Emitter();
  10. this.onDidChange = this._updateEventEmitter.event;
  11. this._textModel = textModel;
  12. this._decorationProvider = decorationProvider;
  13. this._regions = new FoldingRegions(new Uint32Array(0), new Uint32Array(0));
  14. this._editorDecorationIds = [];
  15. this._isInitialized = false;
  16. }
  17. get regions() { return this._regions; }
  18. get textModel() { return this._textModel; }
  19. get isInitialized() { return this._isInitialized; }
  20. toggleCollapseState(toggledRegions) {
  21. if (!toggledRegions.length) {
  22. return;
  23. }
  24. toggledRegions = toggledRegions.sort((r1, r2) => r1.regionIndex - r2.regionIndex);
  25. const processed = {};
  26. this._decorationProvider.changeDecorations(accessor => {
  27. let k = 0; // index from [0 ... this.regions.length]
  28. let dirtyRegionEndLine = -1; // end of the range where decorations need to be updated
  29. let lastHiddenLine = -1; // the end of the last hidden lines
  30. const updateDecorationsUntil = (index) => {
  31. while (k < index) {
  32. const endLineNumber = this._regions.getEndLineNumber(k);
  33. const isCollapsed = this._regions.isCollapsed(k);
  34. if (endLineNumber <= dirtyRegionEndLine) {
  35. accessor.changeDecorationOptions(this._editorDecorationIds[k], this._decorationProvider.getDecorationOption(isCollapsed, endLineNumber <= lastHiddenLine));
  36. }
  37. if (isCollapsed && endLineNumber > lastHiddenLine) {
  38. lastHiddenLine = endLineNumber;
  39. }
  40. k++;
  41. }
  42. };
  43. for (let region of toggledRegions) {
  44. let index = region.regionIndex;
  45. let editorDecorationId = this._editorDecorationIds[index];
  46. if (editorDecorationId && !processed[editorDecorationId]) {
  47. processed[editorDecorationId] = true;
  48. updateDecorationsUntil(index); // update all decorations up to current index using the old dirtyRegionEndLine
  49. let newCollapseState = !this._regions.isCollapsed(index);
  50. this._regions.setCollapsed(index, newCollapseState);
  51. dirtyRegionEndLine = Math.max(dirtyRegionEndLine, this._regions.getEndLineNumber(index));
  52. }
  53. }
  54. updateDecorationsUntil(this._regions.length);
  55. });
  56. this._updateEventEmitter.fire({ model: this, collapseStateChanged: toggledRegions });
  57. }
  58. update(newRegions, blockedLineNumers = []) {
  59. let newEditorDecorations = [];
  60. let isBlocked = (startLineNumber, endLineNumber) => {
  61. for (let blockedLineNumber of blockedLineNumers) {
  62. if (startLineNumber < blockedLineNumber && blockedLineNumber <= endLineNumber) { // first line is visible
  63. return true;
  64. }
  65. }
  66. return false;
  67. };
  68. let lastHiddenLine = -1;
  69. let initRange = (index, isCollapsed) => {
  70. const startLineNumber = newRegions.getStartLineNumber(index);
  71. const endLineNumber = newRegions.getEndLineNumber(index);
  72. if (!isCollapsed) {
  73. isCollapsed = newRegions.isCollapsed(index);
  74. }
  75. if (isCollapsed && isBlocked(startLineNumber, endLineNumber)) {
  76. isCollapsed = false;
  77. }
  78. newRegions.setCollapsed(index, isCollapsed);
  79. const maxColumn = this._textModel.getLineMaxColumn(startLineNumber);
  80. const decorationRange = {
  81. startLineNumber: startLineNumber,
  82. startColumn: Math.max(maxColumn - 1, 1),
  83. endLineNumber: startLineNumber,
  84. endColumn: maxColumn
  85. };
  86. newEditorDecorations.push({ range: decorationRange, options: this._decorationProvider.getDecorationOption(isCollapsed, endLineNumber <= lastHiddenLine) });
  87. if (isCollapsed && endLineNumber > lastHiddenLine) {
  88. lastHiddenLine = endLineNumber;
  89. }
  90. };
  91. let i = 0;
  92. let nextCollapsed = () => {
  93. while (i < this._regions.length) {
  94. let isCollapsed = this._regions.isCollapsed(i);
  95. i++;
  96. if (isCollapsed) {
  97. return i - 1;
  98. }
  99. }
  100. return -1;
  101. };
  102. let k = 0;
  103. let collapsedIndex = nextCollapsed();
  104. while (collapsedIndex !== -1 && k < newRegions.length) {
  105. // get the latest range
  106. let decRange = this._textModel.getDecorationRange(this._editorDecorationIds[collapsedIndex]);
  107. if (decRange) {
  108. let collapsedStartLineNumber = decRange.startLineNumber;
  109. if (decRange.startColumn === Math.max(decRange.endColumn - 1, 1) && this._textModel.getLineMaxColumn(collapsedStartLineNumber) === decRange.endColumn) { // test that the decoration is still covering the full line else it got deleted
  110. while (k < newRegions.length) {
  111. let startLineNumber = newRegions.getStartLineNumber(k);
  112. if (collapsedStartLineNumber >= startLineNumber) {
  113. initRange(k, collapsedStartLineNumber === startLineNumber);
  114. k++;
  115. }
  116. else {
  117. break;
  118. }
  119. }
  120. }
  121. }
  122. collapsedIndex = nextCollapsed();
  123. }
  124. while (k < newRegions.length) {
  125. initRange(k, false);
  126. k++;
  127. }
  128. this._editorDecorationIds = this._decorationProvider.deltaDecorations(this._editorDecorationIds, newEditorDecorations);
  129. this._regions = newRegions;
  130. this._isInitialized = true;
  131. this._updateEventEmitter.fire({ model: this });
  132. }
  133. /**
  134. * Collapse state memento, for persistence only
  135. */
  136. getMemento() {
  137. let collapsedRanges = [];
  138. for (let i = 0; i < this._regions.length; i++) {
  139. if (this._regions.isCollapsed(i)) {
  140. let range = this._textModel.getDecorationRange(this._editorDecorationIds[i]);
  141. if (range) {
  142. let startLineNumber = range.startLineNumber;
  143. let endLineNumber = range.endLineNumber + this._regions.getEndLineNumber(i) - this._regions.getStartLineNumber(i);
  144. collapsedRanges.push({ startLineNumber, endLineNumber });
  145. }
  146. }
  147. }
  148. if (collapsedRanges.length > 0) {
  149. return collapsedRanges;
  150. }
  151. return undefined;
  152. }
  153. /**
  154. * Apply persisted state, for persistence only
  155. */
  156. applyMemento(state) {
  157. if (!Array.isArray(state)) {
  158. return;
  159. }
  160. let toToogle = [];
  161. for (let range of state) {
  162. let region = this.getRegionAtLine(range.startLineNumber);
  163. if (region && !region.isCollapsed) {
  164. toToogle.push(region);
  165. }
  166. }
  167. this.toggleCollapseState(toToogle);
  168. }
  169. dispose() {
  170. this._decorationProvider.deltaDecorations(this._editorDecorationIds, []);
  171. }
  172. getAllRegionsAtLine(lineNumber, filter) {
  173. let result = [];
  174. if (this._regions) {
  175. let index = this._regions.findRange(lineNumber);
  176. let level = 1;
  177. while (index >= 0) {
  178. let current = this._regions.toRegion(index);
  179. if (!filter || filter(current, level)) {
  180. result.push(current);
  181. }
  182. level++;
  183. index = current.parentIndex;
  184. }
  185. }
  186. return result;
  187. }
  188. getRegionAtLine(lineNumber) {
  189. if (this._regions) {
  190. let index = this._regions.findRange(lineNumber);
  191. if (index >= 0) {
  192. return this._regions.toRegion(index);
  193. }
  194. }
  195. return null;
  196. }
  197. getRegionsInside(region, filter) {
  198. let result = [];
  199. let index = region ? region.regionIndex + 1 : 0;
  200. let endLineNumber = region ? region.endLineNumber : Number.MAX_VALUE;
  201. if (filter && filter.length === 2) {
  202. const levelStack = [];
  203. for (let i = index, len = this._regions.length; i < len; i++) {
  204. let current = this._regions.toRegion(i);
  205. if (this._regions.getStartLineNumber(i) < endLineNumber) {
  206. while (levelStack.length > 0 && !current.containedBy(levelStack[levelStack.length - 1])) {
  207. levelStack.pop();
  208. }
  209. levelStack.push(current);
  210. if (filter(current, levelStack.length)) {
  211. result.push(current);
  212. }
  213. }
  214. else {
  215. break;
  216. }
  217. }
  218. }
  219. else {
  220. for (let i = index, len = this._regions.length; i < len; i++) {
  221. let current = this._regions.toRegion(i);
  222. if (this._regions.getStartLineNumber(i) < endLineNumber) {
  223. if (!filter || filter(current)) {
  224. result.push(current);
  225. }
  226. }
  227. else {
  228. break;
  229. }
  230. }
  231. }
  232. return result;
  233. }
  234. }
  235. /**
  236. * Collapse or expand the regions at the given locations
  237. * @param levels The number of levels. Use 1 to only impact the regions at the location, use Number.MAX_VALUE for all levels.
  238. * @param lineNumbers the location of the regions to collapse or expand, or if not set, all regions in the model.
  239. */
  240. export function toggleCollapseState(foldingModel, levels, lineNumbers) {
  241. let toToggle = [];
  242. for (let lineNumber of lineNumbers) {
  243. let region = foldingModel.getRegionAtLine(lineNumber);
  244. if (region) {
  245. const doCollapse = !region.isCollapsed;
  246. toToggle.push(region);
  247. if (levels > 1) {
  248. let regionsInside = foldingModel.getRegionsInside(region, (r, level) => r.isCollapsed !== doCollapse && level < levels);
  249. toToggle.push(...regionsInside);
  250. }
  251. }
  252. }
  253. foldingModel.toggleCollapseState(toToggle);
  254. }
  255. /**
  256. * Collapse or expand the regions at the given locations including all children.
  257. * @param doCollapse Whether to collapse or expand
  258. * @param levels The number of levels. Use 1 to only impact the regions at the location, use Number.MAX_VALUE for all levels.
  259. * @param lineNumbers the location of the regions to collapse or expand, or if not set, all regions in the model.
  260. */
  261. export function setCollapseStateLevelsDown(foldingModel, doCollapse, levels = Number.MAX_VALUE, lineNumbers) {
  262. let toToggle = [];
  263. if (lineNumbers && lineNumbers.length > 0) {
  264. for (let lineNumber of lineNumbers) {
  265. let region = foldingModel.getRegionAtLine(lineNumber);
  266. if (region) {
  267. if (region.isCollapsed !== doCollapse) {
  268. toToggle.push(region);
  269. }
  270. if (levels > 1) {
  271. let regionsInside = foldingModel.getRegionsInside(region, (r, level) => r.isCollapsed !== doCollapse && level < levels);
  272. toToggle.push(...regionsInside);
  273. }
  274. }
  275. }
  276. }
  277. else {
  278. let regionsInside = foldingModel.getRegionsInside(null, (r, level) => r.isCollapsed !== doCollapse && level < levels);
  279. toToggle.push(...regionsInside);
  280. }
  281. foldingModel.toggleCollapseState(toToggle);
  282. }
  283. /**
  284. * Collapse or expand the regions at the given locations including all parents.
  285. * @param doCollapse Whether to collapse or expand
  286. * @param levels The number of levels. Use 1 to only impact the regions at the location, use Number.MAX_VALUE for all levels.
  287. * @param lineNumbers the location of the regions to collapse or expand.
  288. */
  289. export function setCollapseStateLevelsUp(foldingModel, doCollapse, levels, lineNumbers) {
  290. let toToggle = [];
  291. for (let lineNumber of lineNumbers) {
  292. let regions = foldingModel.getAllRegionsAtLine(lineNumber, (region, level) => region.isCollapsed !== doCollapse && level <= levels);
  293. toToggle.push(...regions);
  294. }
  295. foldingModel.toggleCollapseState(toToggle);
  296. }
  297. /**
  298. * Collapse or expand a region at the given locations. If the inner most region is already collapsed/expanded, uses the first parent instead.
  299. * @param doCollapse Whether to collapse or expand
  300. * @param lineNumbers the location of the regions to collapse or expand.
  301. */
  302. export function setCollapseStateUp(foldingModel, doCollapse, lineNumbers) {
  303. let toToggle = [];
  304. for (let lineNumber of lineNumbers) {
  305. let regions = foldingModel.getAllRegionsAtLine(lineNumber, (region) => region.isCollapsed !== doCollapse);
  306. if (regions.length > 0) {
  307. toToggle.push(regions[0]);
  308. }
  309. }
  310. foldingModel.toggleCollapseState(toToggle);
  311. }
  312. /**
  313. * Folds or unfolds all regions that have a given level, except if they contain one of the blocked lines.
  314. * @param foldLevel level. Level == 1 is the top level
  315. * @param doCollapse Whether to collapse or expand
  316. */
  317. export function setCollapseStateAtLevel(foldingModel, foldLevel, doCollapse, blockedLineNumbers) {
  318. let filter = (region, level) => level === foldLevel && region.isCollapsed !== doCollapse && !blockedLineNumbers.some(line => region.containsLine(line));
  319. let toToggle = foldingModel.getRegionsInside(null, filter);
  320. foldingModel.toggleCollapseState(toToggle);
  321. }
  322. /**
  323. * Folds or unfolds all regions, except if they contain or are contained by a region of one of the blocked lines.
  324. * @param doCollapse Whether to collapse or expand
  325. * @param blockedLineNumbers the location of regions to not collapse or expand
  326. */
  327. export function setCollapseStateForRest(foldingModel, doCollapse, blockedLineNumbers) {
  328. let filteredRegions = [];
  329. for (let lineNumber of blockedLineNumbers) {
  330. filteredRegions.push(foldingModel.getAllRegionsAtLine(lineNumber, undefined)[0]);
  331. }
  332. let filter = (region) => filteredRegions.every((filteredRegion) => !filteredRegion.containedBy(region) && !region.containedBy(filteredRegion)) && region.isCollapsed !== doCollapse;
  333. let toToggle = foldingModel.getRegionsInside(null, filter);
  334. foldingModel.toggleCollapseState(toToggle);
  335. }
  336. /**
  337. * Folds all regions for which the lines start with a given regex
  338. * @param foldingModel the folding model
  339. */
  340. export function setCollapseStateForMatchingLines(foldingModel, regExp, doCollapse) {
  341. let editorModel = foldingModel.textModel;
  342. let regions = foldingModel.regions;
  343. let toToggle = [];
  344. for (let i = regions.length - 1; i >= 0; i--) {
  345. if (doCollapse !== regions.isCollapsed(i)) {
  346. let startLineNumber = regions.getStartLineNumber(i);
  347. if (regExp.test(editorModel.getLineContent(startLineNumber))) {
  348. toToggle.push(regions.toRegion(i));
  349. }
  350. }
  351. }
  352. foldingModel.toggleCollapseState(toToggle);
  353. }
  354. /**
  355. * Folds all regions of the given type
  356. * @param foldingModel the folding model
  357. */
  358. export function setCollapseStateForType(foldingModel, type, doCollapse) {
  359. let regions = foldingModel.regions;
  360. let toToggle = [];
  361. for (let i = regions.length - 1; i >= 0; i--) {
  362. if (doCollapse !== regions.isCollapsed(i) && type === regions.getType(i)) {
  363. toToggle.push(regions.toRegion(i));
  364. }
  365. }
  366. foldingModel.toggleCollapseState(toToggle);
  367. }
  368. /**
  369. * Get line to go to for parent fold of current line
  370. * @param lineNumber the current line number
  371. * @param foldingModel the folding model
  372. *
  373. * @return Parent fold start line
  374. */
  375. export function getParentFoldLine(lineNumber, foldingModel) {
  376. let startLineNumber = null;
  377. let foldingRegion = foldingModel.getRegionAtLine(lineNumber);
  378. if (foldingRegion !== null) {
  379. startLineNumber = foldingRegion.startLineNumber;
  380. // If current line is not the start of the current fold, go to top line of current fold. If not, go to parent fold
  381. if (lineNumber === startLineNumber) {
  382. let parentFoldingIdx = foldingRegion.parentIndex;
  383. if (parentFoldingIdx !== -1) {
  384. startLineNumber = foldingModel.regions.getStartLineNumber(parentFoldingIdx);
  385. }
  386. else {
  387. startLineNumber = null;
  388. }
  389. }
  390. }
  391. return startLineNumber;
  392. }
  393. /**
  394. * Get line to go to for previous fold at the same level of current line
  395. * @param lineNumber the current line number
  396. * @param foldingModel the folding model
  397. *
  398. * @return Previous fold start line
  399. */
  400. export function getPreviousFoldLine(lineNumber, foldingModel) {
  401. let foldingRegion = foldingModel.getRegionAtLine(lineNumber);
  402. // If on the folding range start line, go to previous sibling.
  403. if (foldingRegion !== null && foldingRegion.startLineNumber === lineNumber) {
  404. // If current line is not the start of the current fold, go to top line of current fold. If not, go to previous fold.
  405. if (lineNumber !== foldingRegion.startLineNumber) {
  406. return foldingRegion.startLineNumber;
  407. }
  408. else {
  409. // Find min line number to stay within parent.
  410. let expectedParentIndex = foldingRegion.parentIndex;
  411. let minLineNumber = 0;
  412. if (expectedParentIndex !== -1) {
  413. minLineNumber = foldingModel.regions.getStartLineNumber(foldingRegion.parentIndex);
  414. }
  415. // Find fold at same level.
  416. while (foldingRegion !== null) {
  417. if (foldingRegion.regionIndex > 0) {
  418. foldingRegion = foldingModel.regions.toRegion(foldingRegion.regionIndex - 1);
  419. // Keep at same level.
  420. if (foldingRegion.startLineNumber <= minLineNumber) {
  421. return null;
  422. }
  423. else if (foldingRegion.parentIndex === expectedParentIndex) {
  424. return foldingRegion.startLineNumber;
  425. }
  426. }
  427. else {
  428. return null;
  429. }
  430. }
  431. }
  432. }
  433. else {
  434. // Go to last fold that's before the current line.
  435. if (foldingModel.regions.length > 0) {
  436. foldingRegion = foldingModel.regions.toRegion(foldingModel.regions.length - 1);
  437. while (foldingRegion !== null) {
  438. // Found fold before current line.
  439. if (foldingRegion.startLineNumber < lineNumber) {
  440. return foldingRegion.startLineNumber;
  441. }
  442. if (foldingRegion.regionIndex > 0) {
  443. foldingRegion = foldingModel.regions.toRegion(foldingRegion.regionIndex - 1);
  444. }
  445. else {
  446. foldingRegion = null;
  447. }
  448. }
  449. }
  450. }
  451. return null;
  452. }
  453. /**
  454. * Get line to go to next fold at the same level of current line
  455. * @param lineNumber the current line number
  456. * @param foldingModel the folding model
  457. *
  458. * @return Next fold start line
  459. */
  460. export function getNextFoldLine(lineNumber, foldingModel) {
  461. let foldingRegion = foldingModel.getRegionAtLine(lineNumber);
  462. // If on the folding range start line, go to next sibling.
  463. if (foldingRegion !== null && foldingRegion.startLineNumber === lineNumber) {
  464. // Find max line number to stay within parent.
  465. let expectedParentIndex = foldingRegion.parentIndex;
  466. let maxLineNumber = 0;
  467. if (expectedParentIndex !== -1) {
  468. maxLineNumber = foldingModel.regions.getEndLineNumber(foldingRegion.parentIndex);
  469. }
  470. else if (foldingModel.regions.length === 0) {
  471. return null;
  472. }
  473. else {
  474. maxLineNumber = foldingModel.regions.getEndLineNumber(foldingModel.regions.length - 1);
  475. }
  476. // Find fold at same level.
  477. while (foldingRegion !== null) {
  478. if (foldingRegion.regionIndex < foldingModel.regions.length) {
  479. foldingRegion = foldingModel.regions.toRegion(foldingRegion.regionIndex + 1);
  480. // Keep at same level.
  481. if (foldingRegion.startLineNumber >= maxLineNumber) {
  482. return null;
  483. }
  484. else if (foldingRegion.parentIndex === expectedParentIndex) {
  485. return foldingRegion.startLineNumber;
  486. }
  487. }
  488. else {
  489. return null;
  490. }
  491. }
  492. }
  493. else {
  494. // Go to first fold that's after the current line.
  495. if (foldingModel.regions.length > 0) {
  496. foldingRegion = foldingModel.regions.toRegion(0);
  497. while (foldingRegion !== null) {
  498. // Found fold after current line.
  499. if (foldingRegion.startLineNumber > lineNumber) {
  500. return foldingRegion.startLineNumber;
  501. }
  502. if (foldingRegion.regionIndex < foldingModel.regions.length) {
  503. foldingRegion = foldingModel.regions.toRegion(foldingRegion.regionIndex + 1);
  504. }
  505. else {
  506. foldingRegion = null;
  507. }
  508. }
  509. }
  510. }
  511. return null;
  512. }