| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534 |
- /*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
- import { Emitter } from '../../../../base/common/event.js';
- import { FoldingRegions } from './foldingRanges.js';
- import { hash } from '../../../../base/common/hash.js';
- export class FoldingModel {
- constructor(textModel, decorationProvider) {
- this._updateEventEmitter = new Emitter();
- this.onDidChange = this._updateEventEmitter.event;
- this._textModel = textModel;
- this._decorationProvider = decorationProvider;
- this._regions = new FoldingRegions(new Uint32Array(0), new Uint32Array(0));
- this._editorDecorationIds = [];
- }
- get regions() { return this._regions; }
- get textModel() { return this._textModel; }
- toggleCollapseState(toggledRegions) {
- if (!toggledRegions.length) {
- return;
- }
- toggledRegions = toggledRegions.sort((r1, r2) => r1.regionIndex - r2.regionIndex);
- const processed = {};
- this._decorationProvider.changeDecorations(accessor => {
- let k = 0; // index from [0 ... this.regions.length]
- let dirtyRegionEndLine = -1; // end of the range where decorations need to be updated
- let lastHiddenLine = -1; // the end of the last hidden lines
- const updateDecorationsUntil = (index) => {
- while (k < index) {
- const endLineNumber = this._regions.getEndLineNumber(k);
- const isCollapsed = this._regions.isCollapsed(k);
- if (endLineNumber <= dirtyRegionEndLine) {
- const isManual = this.regions.getSource(k) !== 0 /* FoldSource.provider */;
- accessor.changeDecorationOptions(this._editorDecorationIds[k], this._decorationProvider.getDecorationOption(isCollapsed, endLineNumber <= lastHiddenLine, isManual));
- }
- if (isCollapsed && endLineNumber > lastHiddenLine) {
- lastHiddenLine = endLineNumber;
- }
- k++;
- }
- };
- for (const region of toggledRegions) {
- const index = region.regionIndex;
- const editorDecorationId = this._editorDecorationIds[index];
- if (editorDecorationId && !processed[editorDecorationId]) {
- processed[editorDecorationId] = true;
- updateDecorationsUntil(index); // update all decorations up to current index using the old dirtyRegionEndLine
- const newCollapseState = !this._regions.isCollapsed(index);
- this._regions.setCollapsed(index, newCollapseState);
- dirtyRegionEndLine = Math.max(dirtyRegionEndLine, this._regions.getEndLineNumber(index));
- }
- }
- updateDecorationsUntil(this._regions.length);
- });
- this._updateEventEmitter.fire({ model: this, collapseStateChanged: toggledRegions });
- }
- removeManualRanges(ranges) {
- const newFoldingRanges = new Array();
- const intersects = (foldRange) => {
- for (const range of ranges) {
- if (!(range.startLineNumber > foldRange.endLineNumber || foldRange.startLineNumber > range.endLineNumber)) {
- return true;
- }
- }
- return false;
- };
- for (let i = 0; i < this._regions.length; i++) {
- const foldRange = this._regions.toFoldRange(i);
- if (foldRange.source === 0 /* FoldSource.provider */ || !intersects(foldRange)) {
- newFoldingRanges.push(foldRange);
- }
- }
- this.updatePost(FoldingRegions.fromFoldRanges(newFoldingRanges));
- }
- update(newRegions, blockedLineNumers = []) {
- const foldedOrManualRanges = this._currentFoldedOrManualRanges(blockedLineNumers);
- const newRanges = FoldingRegions.sanitizeAndMerge(newRegions, foldedOrManualRanges, this._textModel.getLineCount());
- this.updatePost(FoldingRegions.fromFoldRanges(newRanges));
- }
- updatePost(newRegions) {
- const newEditorDecorations = [];
- let lastHiddenLine = -1;
- for (let index = 0, limit = newRegions.length; index < limit; index++) {
- const startLineNumber = newRegions.getStartLineNumber(index);
- const endLineNumber = newRegions.getEndLineNumber(index);
- const isCollapsed = newRegions.isCollapsed(index);
- const isManual = newRegions.getSource(index) !== 0 /* FoldSource.provider */;
- const decorationRange = {
- startLineNumber: startLineNumber,
- startColumn: this._textModel.getLineMaxColumn(startLineNumber),
- endLineNumber: endLineNumber,
- endColumn: this._textModel.getLineMaxColumn(endLineNumber) + 1
- };
- newEditorDecorations.push({ range: decorationRange, options: this._decorationProvider.getDecorationOption(isCollapsed, endLineNumber <= lastHiddenLine, isManual) });
- if (isCollapsed && endLineNumber > lastHiddenLine) {
- lastHiddenLine = endLineNumber;
- }
- }
- this._decorationProvider.changeDecorations(accessor => this._editorDecorationIds = accessor.deltaDecorations(this._editorDecorationIds, newEditorDecorations));
- this._regions = newRegions;
- this._updateEventEmitter.fire({ model: this });
- }
- _currentFoldedOrManualRanges(blockedLineNumers = []) {
- const isBlocked = (startLineNumber, endLineNumber) => {
- for (const blockedLineNumber of blockedLineNumers) {
- if (startLineNumber < blockedLineNumber && blockedLineNumber <= endLineNumber) { // first line is visible
- return true;
- }
- }
- return false;
- };
- const foldedRanges = [];
- for (let i = 0, limit = this._regions.length; i < limit; i++) {
- let isCollapsed = this.regions.isCollapsed(i);
- const source = this.regions.getSource(i);
- if (isCollapsed || source !== 0 /* FoldSource.provider */) {
- const foldRange = this._regions.toFoldRange(i);
- const decRange = this._textModel.getDecorationRange(this._editorDecorationIds[i]);
- if (decRange) {
- if (isCollapsed && (isBlocked(decRange.startLineNumber, decRange.endLineNumber) || decRange.endLineNumber - decRange.startLineNumber !== foldRange.endLineNumber - foldRange.startLineNumber)) {
- isCollapsed = false; // uncollapse is the range is blocked or there has been lines removed or added
- }
- foldedRanges.push({
- startLineNumber: decRange.startLineNumber,
- endLineNumber: decRange.endLineNumber,
- type: foldRange.type,
- isCollapsed,
- source
- });
- }
- }
- }
- return foldedRanges;
- }
- /**
- * Collapse state memento, for persistence only
- */
- getMemento() {
- const foldedOrManualRanges = this._currentFoldedOrManualRanges();
- const result = [];
- for (let i = 0, limit = foldedOrManualRanges.length; i < limit; i++) {
- const range = foldedOrManualRanges[i];
- const checksum = this._getLinesChecksum(range.startLineNumber + 1, range.endLineNumber);
- result.push({
- startLineNumber: range.startLineNumber,
- endLineNumber: range.endLineNumber,
- isCollapsed: range.isCollapsed,
- source: range.source,
- checksum: checksum
- });
- }
- return (result.length > 0) ? result : undefined;
- }
- /**
- * Apply persisted state, for persistence only
- */
- applyMemento(state) {
- var _a, _b;
- if (!Array.isArray(state)) {
- return;
- }
- const rangesToRestore = [];
- const maxLineNumber = this._textModel.getLineCount();
- for (const range of state) {
- if (range.startLineNumber >= range.endLineNumber || range.startLineNumber < 1 || range.endLineNumber > maxLineNumber) {
- continue;
- }
- const checksum = this._getLinesChecksum(range.startLineNumber + 1, range.endLineNumber);
- if (!range.checksum || checksum === range.checksum) {
- rangesToRestore.push({
- startLineNumber: range.startLineNumber,
- endLineNumber: range.endLineNumber,
- type: undefined,
- isCollapsed: (_a = range.isCollapsed) !== null && _a !== void 0 ? _a : true,
- source: (_b = range.source) !== null && _b !== void 0 ? _b : 0 /* FoldSource.provider */
- });
- }
- }
- const newRanges = FoldingRegions.sanitizeAndMerge(this._regions, rangesToRestore, maxLineNumber);
- this.updatePost(FoldingRegions.fromFoldRanges(newRanges));
- }
- _getLinesChecksum(lineNumber1, lineNumber2) {
- const h = hash(this._textModel.getLineContent(lineNumber1)
- + this._textModel.getLineContent(lineNumber2));
- return h % 1000000; // 6 digits is plenty
- }
- dispose() {
- this._decorationProvider.removeDecorations(this._editorDecorationIds);
- }
- getAllRegionsAtLine(lineNumber, filter) {
- const result = [];
- if (this._regions) {
- let index = this._regions.findRange(lineNumber);
- let level = 1;
- while (index >= 0) {
- const current = this._regions.toRegion(index);
- if (!filter || filter(current, level)) {
- result.push(current);
- }
- level++;
- index = current.parentIndex;
- }
- }
- return result;
- }
- getRegionAtLine(lineNumber) {
- if (this._regions) {
- const index = this._regions.findRange(lineNumber);
- if (index >= 0) {
- return this._regions.toRegion(index);
- }
- }
- return null;
- }
- getRegionsInside(region, filter) {
- const result = [];
- const index = region ? region.regionIndex + 1 : 0;
- const endLineNumber = region ? region.endLineNumber : Number.MAX_VALUE;
- if (filter && filter.length === 2) {
- const levelStack = [];
- for (let i = index, len = this._regions.length; i < len; i++) {
- const current = this._regions.toRegion(i);
- if (this._regions.getStartLineNumber(i) < endLineNumber) {
- while (levelStack.length > 0 && !current.containedBy(levelStack[levelStack.length - 1])) {
- levelStack.pop();
- }
- levelStack.push(current);
- if (filter(current, levelStack.length)) {
- result.push(current);
- }
- }
- else {
- break;
- }
- }
- }
- else {
- for (let i = index, len = this._regions.length; i < len; i++) {
- const current = this._regions.toRegion(i);
- if (this._regions.getStartLineNumber(i) < endLineNumber) {
- if (!filter || filter(current)) {
- result.push(current);
- }
- }
- else {
- break;
- }
- }
- }
- return result;
- }
- }
- /**
- * Collapse or expand the regions at the given locations
- * @param levels The number of levels. Use 1 to only impact the regions at the location, use Number.MAX_VALUE for all levels.
- * @param lineNumbers the location of the regions to collapse or expand, or if not set, all regions in the model.
- */
- export function toggleCollapseState(foldingModel, levels, lineNumbers) {
- const toToggle = [];
- for (const lineNumber of lineNumbers) {
- const region = foldingModel.getRegionAtLine(lineNumber);
- if (region) {
- const doCollapse = !region.isCollapsed;
- toToggle.push(region);
- if (levels > 1) {
- const regionsInside = foldingModel.getRegionsInside(region, (r, level) => r.isCollapsed !== doCollapse && level < levels);
- toToggle.push(...regionsInside);
- }
- }
- }
- foldingModel.toggleCollapseState(toToggle);
- }
- /**
- * Collapse or expand the regions at the given locations including all children.
- * @param doCollapse Whether to collapse or expand
- * @param levels The number of levels. Use 1 to only impact the regions at the location, use Number.MAX_VALUE for all levels.
- * @param lineNumbers the location of the regions to collapse or expand, or if not set, all regions in the model.
- */
- export function setCollapseStateLevelsDown(foldingModel, doCollapse, levels = Number.MAX_VALUE, lineNumbers) {
- const toToggle = [];
- if (lineNumbers && lineNumbers.length > 0) {
- for (const lineNumber of lineNumbers) {
- const region = foldingModel.getRegionAtLine(lineNumber);
- if (region) {
- if (region.isCollapsed !== doCollapse) {
- toToggle.push(region);
- }
- if (levels > 1) {
- const regionsInside = foldingModel.getRegionsInside(region, (r, level) => r.isCollapsed !== doCollapse && level < levels);
- toToggle.push(...regionsInside);
- }
- }
- }
- }
- else {
- const regionsInside = foldingModel.getRegionsInside(null, (r, level) => r.isCollapsed !== doCollapse && level < levels);
- toToggle.push(...regionsInside);
- }
- foldingModel.toggleCollapseState(toToggle);
- }
- /**
- * Collapse or expand the regions at the given locations including all parents.
- * @param doCollapse Whether to collapse or expand
- * @param levels The number of levels. Use 1 to only impact the regions at the location, use Number.MAX_VALUE for all levels.
- * @param lineNumbers the location of the regions to collapse or expand.
- */
- export function setCollapseStateLevelsUp(foldingModel, doCollapse, levels, lineNumbers) {
- const toToggle = [];
- for (const lineNumber of lineNumbers) {
- const regions = foldingModel.getAllRegionsAtLine(lineNumber, (region, level) => region.isCollapsed !== doCollapse && level <= levels);
- toToggle.push(...regions);
- }
- foldingModel.toggleCollapseState(toToggle);
- }
- /**
- * Collapse or expand a region at the given locations. If the inner most region is already collapsed/expanded, uses the first parent instead.
- * @param doCollapse Whether to collapse or expand
- * @param lineNumbers the location of the regions to collapse or expand.
- */
- export function setCollapseStateUp(foldingModel, doCollapse, lineNumbers) {
- const toToggle = [];
- for (const lineNumber of lineNumbers) {
- const regions = foldingModel.getAllRegionsAtLine(lineNumber, (region) => region.isCollapsed !== doCollapse);
- if (regions.length > 0) {
- toToggle.push(regions[0]);
- }
- }
- foldingModel.toggleCollapseState(toToggle);
- }
- /**
- * Folds or unfolds all regions that have a given level, except if they contain one of the blocked lines.
- * @param foldLevel level. Level == 1 is the top level
- * @param doCollapse Whether to collapse or expand
- */
- export function setCollapseStateAtLevel(foldingModel, foldLevel, doCollapse, blockedLineNumbers) {
- const filter = (region, level) => level === foldLevel && region.isCollapsed !== doCollapse && !blockedLineNumbers.some(line => region.containsLine(line));
- const toToggle = foldingModel.getRegionsInside(null, filter);
- foldingModel.toggleCollapseState(toToggle);
- }
- /**
- * Folds or unfolds all regions, except if they contain or are contained by a region of one of the blocked lines.
- * @param doCollapse Whether to collapse or expand
- * @param blockedLineNumbers the location of regions to not collapse or expand
- */
- export function setCollapseStateForRest(foldingModel, doCollapse, blockedLineNumbers) {
- const filteredRegions = [];
- for (const lineNumber of blockedLineNumbers) {
- const regions = foldingModel.getAllRegionsAtLine(lineNumber, undefined);
- if (regions.length > 0) {
- filteredRegions.push(regions[0]);
- }
- }
- const filter = (region) => filteredRegions.every((filteredRegion) => !filteredRegion.containedBy(region) && !region.containedBy(filteredRegion)) && region.isCollapsed !== doCollapse;
- const toToggle = foldingModel.getRegionsInside(null, filter);
- foldingModel.toggleCollapseState(toToggle);
- }
- /**
- * Folds all regions for which the lines start with a given regex
- * @param foldingModel the folding model
- */
- export function setCollapseStateForMatchingLines(foldingModel, regExp, doCollapse) {
- const editorModel = foldingModel.textModel;
- const regions = foldingModel.regions;
- const toToggle = [];
- for (let i = regions.length - 1; i >= 0; i--) {
- if (doCollapse !== regions.isCollapsed(i)) {
- const startLineNumber = regions.getStartLineNumber(i);
- if (regExp.test(editorModel.getLineContent(startLineNumber))) {
- toToggle.push(regions.toRegion(i));
- }
- }
- }
- foldingModel.toggleCollapseState(toToggle);
- }
- /**
- * Folds all regions of the given type
- * @param foldingModel the folding model
- */
- export function setCollapseStateForType(foldingModel, type, doCollapse) {
- const regions = foldingModel.regions;
- const toToggle = [];
- for (let i = regions.length - 1; i >= 0; i--) {
- if (doCollapse !== regions.isCollapsed(i) && type === regions.getType(i)) {
- toToggle.push(regions.toRegion(i));
- }
- }
- foldingModel.toggleCollapseState(toToggle);
- }
- /**
- * Get line to go to for parent fold of current line
- * @param lineNumber the current line number
- * @param foldingModel the folding model
- *
- * @return Parent fold start line
- */
- export function getParentFoldLine(lineNumber, foldingModel) {
- let startLineNumber = null;
- const foldingRegion = foldingModel.getRegionAtLine(lineNumber);
- if (foldingRegion !== null) {
- startLineNumber = foldingRegion.startLineNumber;
- // If current line is not the start of the current fold, go to top line of current fold. If not, go to parent fold
- if (lineNumber === startLineNumber) {
- const parentFoldingIdx = foldingRegion.parentIndex;
- if (parentFoldingIdx !== -1) {
- startLineNumber = foldingModel.regions.getStartLineNumber(parentFoldingIdx);
- }
- else {
- startLineNumber = null;
- }
- }
- }
- return startLineNumber;
- }
- /**
- * Get line to go to for previous fold at the same level of current line
- * @param lineNumber the current line number
- * @param foldingModel the folding model
- *
- * @return Previous fold start line
- */
- export function getPreviousFoldLine(lineNumber, foldingModel) {
- let foldingRegion = foldingModel.getRegionAtLine(lineNumber);
- // If on the folding range start line, go to previous sibling.
- if (foldingRegion !== null && foldingRegion.startLineNumber === lineNumber) {
- // If current line is not the start of the current fold, go to top line of current fold. If not, go to previous fold.
- if (lineNumber !== foldingRegion.startLineNumber) {
- return foldingRegion.startLineNumber;
- }
- else {
- // Find min line number to stay within parent.
- const expectedParentIndex = foldingRegion.parentIndex;
- let minLineNumber = 0;
- if (expectedParentIndex !== -1) {
- minLineNumber = foldingModel.regions.getStartLineNumber(foldingRegion.parentIndex);
- }
- // Find fold at same level.
- while (foldingRegion !== null) {
- if (foldingRegion.regionIndex > 0) {
- foldingRegion = foldingModel.regions.toRegion(foldingRegion.regionIndex - 1);
- // Keep at same level.
- if (foldingRegion.startLineNumber <= minLineNumber) {
- return null;
- }
- else if (foldingRegion.parentIndex === expectedParentIndex) {
- return foldingRegion.startLineNumber;
- }
- }
- else {
- return null;
- }
- }
- }
- }
- else {
- // Go to last fold that's before the current line.
- if (foldingModel.regions.length > 0) {
- foldingRegion = foldingModel.regions.toRegion(foldingModel.regions.length - 1);
- while (foldingRegion !== null) {
- // Found fold before current line.
- if (foldingRegion.startLineNumber < lineNumber) {
- return foldingRegion.startLineNumber;
- }
- if (foldingRegion.regionIndex > 0) {
- foldingRegion = foldingModel.regions.toRegion(foldingRegion.regionIndex - 1);
- }
- else {
- foldingRegion = null;
- }
- }
- }
- }
- return null;
- }
- /**
- * Get line to go to next fold at the same level of current line
- * @param lineNumber the current line number
- * @param foldingModel the folding model
- *
- * @return Next fold start line
- */
- export function getNextFoldLine(lineNumber, foldingModel) {
- let foldingRegion = foldingModel.getRegionAtLine(lineNumber);
- // If on the folding range start line, go to next sibling.
- if (foldingRegion !== null && foldingRegion.startLineNumber === lineNumber) {
- // Find max line number to stay within parent.
- const expectedParentIndex = foldingRegion.parentIndex;
- let maxLineNumber = 0;
- if (expectedParentIndex !== -1) {
- maxLineNumber = foldingModel.regions.getEndLineNumber(foldingRegion.parentIndex);
- }
- else if (foldingModel.regions.length === 0) {
- return null;
- }
- else {
- maxLineNumber = foldingModel.regions.getEndLineNumber(foldingModel.regions.length - 1);
- }
- // Find fold at same level.
- while (foldingRegion !== null) {
- if (foldingRegion.regionIndex < foldingModel.regions.length) {
- foldingRegion = foldingModel.regions.toRegion(foldingRegion.regionIndex + 1);
- // Keep at same level.
- if (foldingRegion.startLineNumber >= maxLineNumber) {
- return null;
- }
- else if (foldingRegion.parentIndex === expectedParentIndex) {
- return foldingRegion.startLineNumber;
- }
- }
- else {
- return null;
- }
- }
- }
- else {
- // Go to first fold that's after the current line.
- if (foldingModel.regions.length > 0) {
- foldingRegion = foldingModel.regions.toRegion(0);
- while (foldingRegion !== null) {
- // Found fold after current line.
- if (foldingRegion.startLineNumber > lineNumber) {
- return foldingRegion.startLineNumber;
- }
- if (foldingRegion.regionIndex < foldingModel.regions.length) {
- foldingRegion = foldingModel.regions.toRegion(foldingRegion.regionIndex + 1);
- }
- else {
- foldingRegion = null;
- }
- }
- }
- }
- return null;
- }
|