foldingModel.ts 10.9 KB
Newer Older
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

A
Alex Dima 已提交
6
import { ITextModel, IModelDecorationOptions, IModelDeltaDecoration, IModelDecorationsChangeAccessor } from 'vs/editor/common/model';
7
import Event, { Emitter } from 'vs/base/common/event';
8
import { FoldingRanges, ILineRange, FoldingRegion } from './foldingRanges';
9

10
export interface IDecorationProvider {
M
Martin Aeschlimann 已提交
11
	getDecorationOption(isCollapsed: boolean): IModelDecorationOptions;
12 13
	deltaDecorations(oldDecorations: string[], newDecorations: IModelDeltaDecoration[]): string[];
	changeDecorations<T>(callback: (changeAccessor: IModelDecorationsChangeAccessor) => T): T;
14
}
15

16 17 18 19
export interface FoldingModelChangeEvent {
	model: FoldingModel;
	collapseStateChanged?: FoldingRegion[];
}
20

21
export type CollapseMemento = ILineRange[];
22

23
export class FoldingModel {
A
Alex Dima 已提交
24
	private _textModel: ITextModel;
25
	private _decorationProvider: IDecorationProvider;
26

27
	private _ranges: FoldingRanges;
M
Martin Aeschlimann 已提交
28
	private _editorDecorationIds: string[];
29
	private _isInitialized: boolean;
30

31
	private _updateEventEmitter = new Emitter<FoldingModelChangeEvent>();
32

M
Martin Aeschlimann 已提交
33
	public get ranges(): FoldingRanges { return this._ranges; }
34
	public get onDidChange(): Event<FoldingModelChangeEvent> { return this._updateEventEmitter.event; }
35
	public get textModel() { return this._textModel; }
36
	public get isInitialized() { return this._isInitialized; }
37

A
Alex Dima 已提交
38
	constructor(textModel: ITextModel, decorationProvider: IDecorationProvider) {
39 40
		this._textModel = textModel;
		this._decorationProvider = decorationProvider;
M
Martin Aeschlimann 已提交
41
		this._ranges = new FoldingRanges(new Uint32Array(0), new Uint32Array(0));
42
		this._editorDecorationIds = [];
43
		this._isInitialized = false;
44 45
	}

46 47 48 49 50
	public toggleCollapseState(regions: FoldingRegion[]) {
		if (!regions.length) {
			return;
		}
		let processed = {};
51
		this._decorationProvider.changeDecorations(accessor => {
52
			for (let region of regions) {
M
Martin Aeschlimann 已提交
53 54 55 56 57 58 59
				let index = region.regionIndex;
				let editorDecorationId = this._editorDecorationIds[index];
				if (editorDecorationId && !processed[editorDecorationId]) {
					processed[editorDecorationId] = true;
					let newCollapseState = !this._ranges.isCollapsed(index);
					this._ranges.setCollapsed(index, newCollapseState);
					accessor.changeDecorationOptions(editorDecorationId, this._decorationProvider.getDecorationOption(newCollapseState));
60 61 62 63
				}
			}
		});
		this._updateEventEmitter.fire({ model: this, collapseStateChanged: regions });
64 65
	}

66
	public update(newRanges: FoldingRanges, blockedLineNumers: number[] = []): void {
67 68
		let newEditorDecorations = [];

69 70 71 72 73 74 75 76 77
		let isBlocked = (startLineNumber, endLineNumber) => {
			for (let blockedLineNumber of blockedLineNumers) {
				if (startLineNumber < blockedLineNumber && blockedLineNumber <= endLineNumber) { // first line is visible
					return true;
				}
			}
			return false;
		};

M
Martin Aeschlimann 已提交
78 79
		let initRange = (index: number, isCollapsed: boolean) => {
			let startLineNumber = newRanges.getStartLineNumber(index);
80 81 82 83
			if (isCollapsed && isBlocked(startLineNumber, newRanges.getEndLineNumber(index))) {
				isCollapsed = false;
			}
			newRanges.setCollapsed(index, isCollapsed);
84 85 86 87 88 89 90
			let maxColumn = this._textModel.getLineMaxColumn(startLineNumber);
			let decorationRange = {
				startLineNumber: startLineNumber,
				startColumn: maxColumn,
				endLineNumber: startLineNumber,
				endColumn: maxColumn
			};
M
Martin Aeschlimann 已提交
91
			newEditorDecorations.push({ range: decorationRange, options: this._decorationProvider.getDecorationOption(isCollapsed) });
92 93
		};

M
Martin Aeschlimann 已提交
94 95 96 97 98 99 100 101
		let i = 0;
		let nextCollapsed = () => {
			while (i < this._ranges.length) {
				let isCollapsed = this._ranges.isCollapsed(i);
				i++;
				if (isCollapsed) {
					return i - 1;
				}
102
			}
M
Martin Aeschlimann 已提交
103 104 105 106 107 108 109 110 111 112
			return -1;
		};

		let k = 0;
		let collapsedIndex = nextCollapsed();
		while (collapsedIndex !== -1 && k < newRanges.length) {
			// get the latest range
			let decRange = this._textModel.getDecorationRange(this._editorDecorationIds[collapsedIndex]);
			if (decRange) {
				let collapsedStartLineNumber = decRange.startLineNumber;
113 114 115 116 117 118 119 120 121
				if (this._textModel.getLineMaxColumn(collapsedStartLineNumber) === decRange.startColumn) { // test that the decoration is still at the end otherwise it got deleted
					while (k < newRanges.length) {
						let startLineNumber = newRanges.getStartLineNumber(k);
						if (collapsedStartLineNumber >= startLineNumber) {
							initRange(k, collapsedStartLineNumber === startLineNumber);
							k++;
						} else {
							break;
						}
M
Martin Aeschlimann 已提交
122
					}
123 124
				}
			}
M
Martin Aeschlimann 已提交
125
			collapsedIndex = nextCollapsed();
126 127
		}
		while (k < newRanges.length) {
M
Martin Aeschlimann 已提交
128
			initRange(k, false);
129 130 131
			k++;
		}

132
		this._editorDecorationIds = this._decorationProvider.deltaDecorations(this._editorDecorationIds, newEditorDecorations);
133
		this._ranges = newRanges;
134
		this._isInitialized = true;
135
		this._updateEventEmitter.fire({ model: this });
136 137
	}

138
	/**
139
	 * Collapse state memento, for persistence only
140
	 */
141
	public getMemento(): CollapseMemento {
142
		let collapsedRanges: ILineRange[] = [];
M
Martin Aeschlimann 已提交
143 144 145
		for (let i = 0; i < this._ranges.length; i++) {
			if (this._ranges.isCollapsed(i)) {
				let range = this._textModel.getDecorationRange(this._editorDecorationIds[i]);
146 147
				if (range) {
					let startLineNumber = range.startLineNumber;
M
Martin Aeschlimann 已提交
148
					let endLineNumber = range.endLineNumber + this._ranges.getEndLineNumber(i) - this._ranges.getStartLineNumber(i);
149 150 151 152 153 154 155 156 157 158 159 160 161
					collapsedRanges.push({ startLineNumber, endLineNumber });
				}
			}
		}
		if (collapsedRanges.length > 0) {
			return collapsedRanges;
		}
		return null;
	}

	/**
	 * Apply persisted state, for persistence only
	 */
162
	public applyMemento(state: CollapseMemento) {
163 164 165 166 167 168 169 170 171 172 173 174 175
		if (!Array.isArray(state)) {
			return;
		}
		let toToogle: FoldingRegion[] = [];
		for (let range of state) {
			let region = this.getRegionAtLine(range.startLineNumber);
			if (region && !region.isCollapsed) {
				toToogle.push(region);
			}
		}
		this.toggleCollapseState(toToogle);
	}

176
	public dispose() {
177
		this._decorationProvider.deltaDecorations(this._editorDecorationIds, []);
178 179
	}

180
	getAllRegionsAtLine(lineNumber: number, filter?: (r: FoldingRegion, level: number) => boolean): FoldingRegion[] {
181
		let result: FoldingRegion[] = [];
182 183 184 185
		if (this._ranges) {
			let index = this._ranges.findRange(lineNumber);
			let level = 1;
			while (index >= 0) {
186
				let current = this._ranges.toRegion(index);
187
				if (!filter || filter(current, level)) {
188 189
					result.push(current);
				}
190 191
				level++;
				index = current.parentIndex;
192
			}
193
		}
194
		return result;
195 196
	}

197
	getRegionAtLine(lineNumber: number): FoldingRegion {
198 199 200
		if (this._ranges) {
			let index = this._ranges.findRange(lineNumber);
			if (index >= 0) {
201
				return this._ranges.toRegion(index);
202
			}
203
		}
204
		return null;
205 206
	}

207
	getRegionsInside(region: FoldingRegion, filter?: (r: FoldingRegion, level?: number) => boolean): FoldingRegion[] {
208 209
		let result = [];
		let trackLevel = filter && filter.length === 2;
210 211 212
		let levelStack: FoldingRegion[] = trackLevel ? [] : null;
		let index = region ? region.regionIndex + 1 : 0;
		let endLineNumber = region ? region.endLineNumber : Number.MAX_VALUE;
213
		for (let i = index, len = this._ranges.length; i < len; i++) {
214
			let current = this._ranges.toRegion(i);
215
			if (this._ranges.getStartLineNumber(i) < endLineNumber) {
216 217 218 219 220 221
				if (trackLevel) {
					while (levelStack.length > 0 && !current.containedBy(levelStack[levelStack.length - 1])) {
						levelStack.pop();
					}
					levelStack.push(current);
					if (filter(current, levelStack.length)) {
222 223
						result.push(current);
					}
224 225
				} else if (!filter || filter(current)) {
					result.push(current);
226 227 228 229 230 231
				}
			} else {
				break;
			}
		}
		return result;
232 233
	}

234
}
235 236 237



238 239 240 241 242 243 244
/**
 * Collapse or expand the regions at the given locations including all children.
 * @param doCollapse Wheter to collase 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: FoldingModel, doCollapse: boolean, levels = Number.MAX_VALUE, lineNumbers?: number[]) {
245
	let toToggle = [];
246 247 248 249
	if (lineNumbers && lineNumbers.length > 0) {
		for (let lineNumber of lineNumbers) {
			let region = foldingModel.getRegionAtLine(lineNumber);
			if (region) {
250
				if (region.isCollapsed !== doCollapse) {
251 252
					toToggle.push(region);
				}
253 254 255 256
				if (levels > 1) {
					let regionsInside = foldingModel.getRegionsInside(region, (r, level) => r.isCollapsed !== doCollapse && level < levels);
					toToggle.push(...regionsInside);
				}
257 258
			}
		}
259 260 261
	} else {
		let regionsInside = foldingModel.getRegionsInside(null, (r, level) => r.isCollapsed !== doCollapse && level < levels);
		toToggle.push(...regionsInside);
262
	}
263 264
	foldingModel.toggleCollapseState(toToggle);
}
265

266
/**
267
 * Collapse or expand the regions at the given locations including all parents.
268
 * @param doCollapse Wheter to collase or expand
269 270
 * @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.
271
 */
272
export function setCollapseStateLevelsUp(foldingModel: FoldingModel, doCollapse: boolean, levels: number, lineNumbers: number[]) {
273
	let toToggle = [];
274 275 276
	for (let lineNumber of lineNumbers) {
		let regions = foldingModel.getAllRegionsAtLine(lineNumber, (region, level) => region.isCollapsed !== doCollapse && level <= levels);
		toToggle.push(...regions);
277
	}
278 279 280 281 282 283 284 285 286 287
	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 Wheter to collase or expand
* @param blockedLineNumbers
*/
export function setCollapseStateAtLevel(foldingModel: FoldingModel, foldLevel: number, doCollapse: boolean, blockedLineNumbers: number[]): void {
M
Martin Aeschlimann 已提交
288
	let filter = (region: FoldingRegion, level: number) => level === foldLevel && region.isCollapsed !== doCollapse && !blockedLineNumbers.some(line => region.containsLine(line));
289
	let toToggle = foldingModel.getRegionsInside(null, filter);
290
	foldingModel.toggleCollapseState(toToggle);
291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
}

/**
 * Folds all regions for which the lines start with a given regex
 * @param foldingModel the folding model
 */
export function setCollapseStateForMatchingLines(foldingModel: FoldingModel, regExp: RegExp, doCollapse: boolean): void {
	let editorModel = foldingModel.textModel;
	let ranges = foldingModel.ranges;
	let toToggle = [];
	for (let i = ranges.length - 1; i >= 0; i--) {
		if (doCollapse !== ranges.isCollapsed(i)) {
			let startLineNumber = ranges.getStartLineNumber(i);
			if (regExp.test(editorModel.getLineContent(startLineNumber))) {
				toToggle.push(ranges.toRegion(i));
			}
		}
	}
	foldingModel.toggleCollapseState(toToggle);
310
}