Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/notebook/browser/viewModel/foldingModel.ts
3296 views
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
6
import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js';
7
import { Emitter, Event } from '../../../../../base/common/event.js';
8
import { DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js';
9
import { marked } from '../../../../../base/common/marked/marked.js';
10
import { TrackedRangeStickiness } from '../../../../../editor/common/model.js';
11
import { FoldingLimitReporter } from '../../../../../editor/contrib/folding/browser/folding.js';
12
import { FoldingRegion, FoldingRegions } from '../../../../../editor/contrib/folding/browser/foldingRanges.js';
13
import { IFoldingRangeData, sanitizeRanges } from '../../../../../editor/contrib/folding/browser/syntaxRangeProvider.js';
14
import { INotebookViewModel } from '../notebookBrowser.js';
15
import { CellKind } from '../../common/notebookCommon.js';
16
import { cellRangesToIndexes, ICellRange } from '../../common/notebookRange.js';
17
18
type RegionFilter = (r: FoldingRegion) => boolean;
19
type RegionFilterWithLevel = (r: FoldingRegion, level: number) => boolean;
20
21
const foldingRangeLimit: FoldingLimitReporter = {
22
limit: 5000,
23
update: () => { }
24
};
25
26
export class FoldingModel implements IDisposable {
27
private _viewModel: INotebookViewModel | null = null;
28
private readonly _viewModelStore = new DisposableStore();
29
private _regions: FoldingRegions;
30
get regions() {
31
return this._regions;
32
}
33
34
private readonly _onDidFoldingRegionChanges = new Emitter<void>();
35
readonly onDidFoldingRegionChanged: Event<void> = this._onDidFoldingRegionChanges.event;
36
37
private _foldingRangeDecorationIds: string[] = [];
38
39
constructor() {
40
this._regions = new FoldingRegions(new Uint32Array(0), new Uint32Array(0));
41
}
42
43
dispose() {
44
this._onDidFoldingRegionChanges.dispose();
45
this._viewModelStore.dispose();
46
}
47
48
detachViewModel() {
49
this._viewModelStore.clear();
50
this._viewModel = null;
51
}
52
53
attachViewModel(model: INotebookViewModel) {
54
this._viewModel = model;
55
56
this._viewModelStore.add(this._viewModel.onDidChangeViewCells(() => {
57
this.recompute();
58
}));
59
60
this._viewModelStore.add(this._viewModel.onDidChangeSelection(() => {
61
if (!this._viewModel) {
62
return;
63
}
64
65
const indexes = cellRangesToIndexes(this._viewModel.getSelections());
66
67
let changed = false;
68
69
indexes.forEach(index => {
70
let regionIndex = this.regions.findRange(index + 1);
71
72
while (regionIndex !== -1) {
73
if (this._regions.isCollapsed(regionIndex) && index > this._regions.getStartLineNumber(regionIndex) - 1) {
74
this._regions.setCollapsed(regionIndex, false);
75
changed = true;
76
}
77
regionIndex = this._regions.getParentIndex(regionIndex);
78
}
79
});
80
81
if (changed) {
82
this._onDidFoldingRegionChanges.fire();
83
}
84
85
}));
86
87
this.recompute();
88
}
89
90
getRegionAtLine(lineNumber: number): FoldingRegion | null {
91
if (this._regions) {
92
const index = this._regions.findRange(lineNumber);
93
if (index >= 0) {
94
return this._regions.toRegion(index);
95
}
96
}
97
return null;
98
}
99
100
getRegionsInside(region: FoldingRegion | null, filter?: RegionFilter | RegionFilterWithLevel): FoldingRegion[] {
101
const result: FoldingRegion[] = [];
102
const index = region ? region.regionIndex + 1 : 0;
103
const endLineNumber = region ? region.endLineNumber : Number.MAX_VALUE;
104
105
if (filter && filter.length === 2) {
106
const levelStack: FoldingRegion[] = [];
107
for (let i = index, len = this._regions.length; i < len; i++) {
108
const current = this._regions.toRegion(i);
109
if (this._regions.getStartLineNumber(i) < endLineNumber) {
110
while (levelStack.length > 0 && !current.containedBy(levelStack[levelStack.length - 1])) {
111
levelStack.pop();
112
}
113
levelStack.push(current);
114
if (filter(current, levelStack.length)) {
115
result.push(current);
116
}
117
} else {
118
break;
119
}
120
}
121
} else {
122
for (let i = index, len = this._regions.length; i < len; i++) {
123
const current = this._regions.toRegion(i);
124
if (this._regions.getStartLineNumber(i) < endLineNumber) {
125
if (!filter || (filter as RegionFilter)(current)) {
126
result.push(current);
127
}
128
} else {
129
break;
130
}
131
}
132
}
133
return result;
134
}
135
136
getAllRegionsAtLine(lineNumber: number, filter?: (r: FoldingRegion, level: number) => boolean): FoldingRegion[] {
137
const result: FoldingRegion[] = [];
138
if (this._regions) {
139
let index = this._regions.findRange(lineNumber);
140
let level = 1;
141
while (index >= 0) {
142
const current = this._regions.toRegion(index);
143
if (!filter || filter(current, level)) {
144
result.push(current);
145
}
146
level++;
147
index = current.parentIndex;
148
}
149
}
150
return result;
151
}
152
153
setCollapsed(index: number, newState: boolean) {
154
this._regions.setCollapsed(index, newState);
155
}
156
157
recompute() {
158
if (!this._viewModel) {
159
return;
160
}
161
162
const viewModel = this._viewModel;
163
const cells = viewModel.viewCells;
164
const stack: { index: number; level: number; endIndex: number }[] = [];
165
166
for (let i = 0; i < cells.length; i++) {
167
const cell = cells[i];
168
169
if (cell.cellKind !== CellKind.Markup || cell.language !== 'markdown') {
170
continue;
171
}
172
173
const minDepth = Math.min(7, ...Array.from(getMarkdownHeadersInCell(cell.getText()), header => header.depth));
174
if (minDepth < 7) {
175
// header 1 to 6
176
stack.push({ index: i, level: minDepth, endIndex: 0 });
177
}
178
}
179
180
// calculate folding ranges
181
const rawFoldingRanges: IFoldingRangeData[] = stack.map((entry, startIndex) => {
182
let end: number | undefined = undefined;
183
for (let i = startIndex + 1; i < stack.length; ++i) {
184
if (stack[i].level <= entry.level) {
185
end = stack[i].index - 1;
186
break;
187
}
188
}
189
190
const endIndex = end !== undefined ? end : cells.length - 1;
191
192
// one based
193
return {
194
start: entry.index + 1,
195
end: endIndex + 1,
196
rank: 1
197
};
198
}).filter(range => range.start !== range.end);
199
200
const newRegions = sanitizeRanges(rawFoldingRanges, foldingRangeLimit);
201
202
// restore collased state
203
let i = 0;
204
const nextCollapsed = () => {
205
while (i < this._regions.length) {
206
const isCollapsed = this._regions.isCollapsed(i);
207
i++;
208
if (isCollapsed) {
209
return i - 1;
210
}
211
}
212
return -1;
213
};
214
215
let k = 0;
216
let collapsedIndex = nextCollapsed();
217
218
while (collapsedIndex !== -1 && k < newRegions.length) {
219
// get the latest range
220
const decRange = viewModel.getTrackedRange(this._foldingRangeDecorationIds[collapsedIndex]);
221
if (decRange) {
222
const collasedStartIndex = decRange.start;
223
224
while (k < newRegions.length) {
225
const startIndex = newRegions.getStartLineNumber(k) - 1;
226
if (collasedStartIndex >= startIndex) {
227
newRegions.setCollapsed(k, collasedStartIndex === startIndex);
228
k++;
229
} else {
230
break;
231
}
232
}
233
}
234
collapsedIndex = nextCollapsed();
235
}
236
237
while (k < newRegions.length) {
238
newRegions.setCollapsed(k, false);
239
k++;
240
}
241
242
const cellRanges: ICellRange[] = [];
243
for (let i = 0; i < newRegions.length; i++) {
244
const region = newRegions.toRegion(i);
245
cellRanges.push({ start: region.startLineNumber - 1, end: region.endLineNumber - 1 });
246
}
247
248
// remove old tracked ranges and add new ones
249
// TODO@rebornix, implement delta
250
this._foldingRangeDecorationIds.forEach(id => viewModel.setTrackedRange(id, null, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter));
251
this._foldingRangeDecorationIds = cellRanges.map(region => viewModel.setTrackedRange(null, region, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter)).filter(str => str !== null) as string[];
252
253
this._regions = newRegions;
254
this._onDidFoldingRegionChanges.fire();
255
}
256
257
getMemento(): ICellRange[] {
258
const collapsedRanges: ICellRange[] = [];
259
let i = 0;
260
while (i < this._regions.length) {
261
const isCollapsed = this._regions.isCollapsed(i);
262
263
if (isCollapsed) {
264
const region = this._regions.toRegion(i);
265
collapsedRanges.push({ start: region.startLineNumber - 1, end: region.endLineNumber - 1 });
266
}
267
268
i++;
269
}
270
271
return collapsedRanges;
272
}
273
274
public applyMemento(state: ICellRange[]): boolean {
275
if (!this._viewModel) {
276
return false;
277
}
278
279
let i = 0;
280
let k = 0;
281
282
while (k < state.length && i < this._regions.length) {
283
// get the latest range
284
const decRange = this._viewModel.getTrackedRange(this._foldingRangeDecorationIds[i]);
285
if (decRange) {
286
const collasedStartIndex = state[k].start;
287
288
while (i < this._regions.length) {
289
const startIndex = this._regions.getStartLineNumber(i) - 1;
290
if (collasedStartIndex >= startIndex) {
291
this._regions.setCollapsed(i, collasedStartIndex === startIndex);
292
i++;
293
} else {
294
break;
295
}
296
}
297
}
298
k++;
299
}
300
301
while (i < this._regions.length) {
302
this._regions.setCollapsed(i, false);
303
i++;
304
}
305
306
return true;
307
}
308
}
309
310
export function updateFoldingStateAtIndex(foldingModel: FoldingModel, index: number, collapsed: boolean) {
311
const range = foldingModel.regions.findRange(index + 1);
312
foldingModel.setCollapsed(range, collapsed);
313
}
314
315
export function* getMarkdownHeadersInCell(cellContent: string): Iterable<{ readonly depth: number; readonly text: string }> {
316
for (const token of marked.lexer(cellContent, { gfm: true })) {
317
if (token.type === 'heading') {
318
yield {
319
depth: token.depth,
320
text: renderAsPlaintext({ value: token.raw }).trim()
321
};
322
}
323
}
324
}
325
326