Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/notebook/browser/controller/foldingController.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 { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
7
import { NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR } from '../../common/notebookContextKeys.js';
8
import { INotebookEditor, INotebookEditorMouseEvent, INotebookEditorContribution, getNotebookEditorFromEditorPane, CellFoldingState } from '../notebookBrowser.js';
9
import { FoldingModel } from '../viewModel/foldingModel.js'; import { CellKind } from '../../common/notebookCommon.js';
10
import { ICellRange } from '../../common/notebookRange.js';
11
import { registerNotebookContribution } from '../notebookEditorExtensions.js';
12
import { registerAction2, Action2 } from '../../../../../platform/actions/common/actions.js';
13
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
14
import { InputFocusedContextKey } from '../../../../../platform/contextkey/common/contextkeys.js';
15
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
16
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
17
import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
18
import { IEditorService } from '../../../../services/editor/common/editorService.js';
19
import { NOTEBOOK_ACTIONS_CATEGORY } from './coreActions.js';
20
import { localize, localize2 } from '../../../../../nls.js';
21
import { FoldingRegion } from '../../../../../editor/contrib/folding/browser/foldingRanges.js';
22
import { ICommandMetadata } from '../../../../../platform/commands/common/commands.js';
23
import { NotebookViewModel } from '../viewModel/notebookViewModelImpl.js';
24
25
export class FoldingController extends Disposable implements INotebookEditorContribution {
26
static id: string = 'workbench.notebook.foldingController';
27
28
private _foldingModel: FoldingModel | null = null;
29
private readonly _localStore = this._register(new DisposableStore());
30
31
constructor(private readonly _notebookEditor: INotebookEditor) {
32
super();
33
34
this._register(this._notebookEditor.onMouseUp(e => { this.onMouseUp(e); }));
35
36
this._register(this._notebookEditor.onDidChangeModel(() => {
37
this._localStore.clear();
38
39
if (!this._notebookEditor.hasModel()) {
40
return;
41
}
42
43
this._localStore.add(this._notebookEditor.onDidChangeCellState(e => {
44
if (e.source.editStateChanged && e.cell.cellKind === CellKind.Markup) {
45
this._foldingModel?.recompute();
46
}
47
}));
48
49
this._foldingModel = new FoldingModel();
50
this._localStore.add(this._foldingModel);
51
this._foldingModel.attachViewModel(this._notebookEditor.getViewModel());
52
53
this._localStore.add(this._foldingModel.onDidFoldingRegionChanged(() => {
54
this._updateEditorFoldingRanges();
55
}));
56
}));
57
}
58
59
saveViewState(): ICellRange[] {
60
return this._foldingModel?.getMemento() || [];
61
}
62
63
restoreViewState(state: ICellRange[] | undefined) {
64
this._foldingModel?.applyMemento(state || []);
65
this._updateEditorFoldingRanges();
66
}
67
68
setFoldingStateDown(index: number, state: CellFoldingState, levels: number) {
69
const doCollapse = state === CellFoldingState.Collapsed;
70
const region = this._foldingModel!.getRegionAtLine(index + 1);
71
const regions: FoldingRegion[] = [];
72
if (region) {
73
if (region.isCollapsed !== doCollapse) {
74
regions.push(region);
75
}
76
if (levels > 1) {
77
const regionsInside = this._foldingModel!.getRegionsInside(region, (r, level: number) => r.isCollapsed !== doCollapse && level < levels);
78
regions.push(...regionsInside);
79
}
80
}
81
82
regions.forEach(r => this._foldingModel!.setCollapsed(r.regionIndex, state === CellFoldingState.Collapsed));
83
this._updateEditorFoldingRanges();
84
}
85
86
setFoldingStateUp(index: number, state: CellFoldingState, levels: number) {
87
if (!this._foldingModel) {
88
return;
89
}
90
91
const regions = this._foldingModel.getAllRegionsAtLine(index + 1, (region, level) => region.isCollapsed !== (state === CellFoldingState.Collapsed) && level <= levels);
92
regions.forEach(r => this._foldingModel!.setCollapsed(r.regionIndex, state === CellFoldingState.Collapsed));
93
this._updateEditorFoldingRanges();
94
}
95
96
private _updateEditorFoldingRanges() {
97
if (!this._foldingModel) {
98
return;
99
}
100
101
if (!this._notebookEditor.hasModel()) {
102
return;
103
}
104
105
const vm = this._notebookEditor.getViewModel() as NotebookViewModel;
106
107
vm.updateFoldingRanges(this._foldingModel.regions);
108
const hiddenRanges = vm.getHiddenRanges();
109
this._notebookEditor.setHiddenAreas(hiddenRanges);
110
}
111
112
onMouseUp(e: INotebookEditorMouseEvent) {
113
if (!e.event.target) {
114
return;
115
}
116
117
if (!this._notebookEditor.hasModel()) {
118
return;
119
}
120
121
const viewModel = this._notebookEditor.getViewModel() as NotebookViewModel;
122
const target = e.event.target as HTMLElement;
123
124
if (target.classList.contains('codicon-notebook-collapsed') || target.classList.contains('codicon-notebook-expanded')) {
125
const parent = target.parentElement as HTMLElement;
126
127
if (!parent.classList.contains('notebook-folding-indicator')) {
128
return;
129
}
130
131
// folding icon
132
133
const cellViewModel = e.target;
134
const modelIndex = viewModel.getCellIndex(cellViewModel);
135
const state = viewModel.getFoldingState(modelIndex);
136
137
if (state === CellFoldingState.None) {
138
return;
139
}
140
141
this.setFoldingStateUp(modelIndex, state === CellFoldingState.Collapsed ? CellFoldingState.Expanded : CellFoldingState.Collapsed, 1);
142
this._notebookEditor.focusElement(cellViewModel);
143
}
144
145
return;
146
}
147
148
recompute() {
149
this._foldingModel?.recompute();
150
}
151
}
152
153
registerNotebookContribution(FoldingController.id, FoldingController);
154
155
156
const NOTEBOOK_FOLD_COMMAND_LABEL = localize('fold.cell', "Fold Cell");
157
const NOTEBOOK_UNFOLD_COMMAND_LABEL = localize2('unfold.cell', "Unfold Cell");
158
159
const FOLDING_COMMAND_ARGS: Pick<ICommandMetadata, 'args'> = {
160
args: [{
161
isOptional: true,
162
name: 'index',
163
description: 'The cell index',
164
schema: {
165
'type': 'object',
166
'required': ['index', 'direction'],
167
'properties': {
168
'index': {
169
'type': 'number'
170
},
171
'direction': {
172
'type': 'string',
173
'enum': ['up', 'down'],
174
'default': 'down'
175
},
176
'levels': {
177
'type': 'number',
178
'default': 1
179
},
180
}
181
}
182
}]
183
};
184
185
registerAction2(class extends Action2 {
186
constructor() {
187
super({
188
id: 'notebook.fold',
189
title: localize2('fold.cell', "Fold Cell"),
190
category: NOTEBOOK_ACTIONS_CATEGORY,
191
keybinding: {
192
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)),
193
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.BracketLeft,
194
mac: {
195
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.BracketLeft,
196
secondary: [KeyCode.LeftArrow],
197
},
198
secondary: [KeyCode.LeftArrow],
199
weight: KeybindingWeight.WorkbenchContrib
200
},
201
metadata: {
202
description: NOTEBOOK_FOLD_COMMAND_LABEL,
203
args: FOLDING_COMMAND_ARGS.args
204
},
205
precondition: NOTEBOOK_IS_ACTIVE_EDITOR,
206
f1: true
207
});
208
}
209
210
async run(accessor: ServicesAccessor, args?: { index: number; levels: number; direction: 'up' | 'down' }): Promise<void> {
211
const editorService = accessor.get(IEditorService);
212
213
const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane);
214
if (!editor) {
215
return;
216
}
217
218
if (!editor.hasModel()) {
219
return;
220
}
221
222
const levels = args && args.levels || 1;
223
const direction = args && args.direction === 'up' ? 'up' : 'down';
224
let index: number | undefined = undefined;
225
226
if (args) {
227
index = args.index;
228
} else {
229
const activeCell = editor.getActiveCell();
230
if (!activeCell) {
231
return;
232
}
233
index = editor.getCellIndex(activeCell);
234
}
235
236
const controller = editor.getContribution<FoldingController>(FoldingController.id);
237
if (index !== undefined) {
238
const targetCell = (index < 0 || index >= editor.getLength()) ? undefined : editor.cellAt(index);
239
if (targetCell?.cellKind === CellKind.Code && direction === 'down') {
240
return;
241
}
242
243
if (direction === 'up') {
244
controller.setFoldingStateUp(index, CellFoldingState.Collapsed, levels);
245
} else {
246
controller.setFoldingStateDown(index, CellFoldingState.Collapsed, levels);
247
}
248
249
const viewIndex = editor.getViewModel().getNearestVisibleCellIndexUpwards(index);
250
editor.focusElement(editor.cellAt(viewIndex));
251
}
252
}
253
});
254
255
registerAction2(class extends Action2 {
256
constructor() {
257
super({
258
id: 'notebook.unfold',
259
title: NOTEBOOK_UNFOLD_COMMAND_LABEL,
260
category: NOTEBOOK_ACTIONS_CATEGORY,
261
keybinding: {
262
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)),
263
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.BracketRight,
264
mac: {
265
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.BracketRight,
266
secondary: [KeyCode.RightArrow],
267
},
268
secondary: [KeyCode.RightArrow],
269
weight: KeybindingWeight.WorkbenchContrib
270
},
271
metadata: {
272
description: NOTEBOOK_UNFOLD_COMMAND_LABEL,
273
args: FOLDING_COMMAND_ARGS.args
274
},
275
precondition: NOTEBOOK_IS_ACTIVE_EDITOR,
276
f1: true
277
});
278
}
279
280
async run(accessor: ServicesAccessor, args?: { index: number; levels: number; direction: 'up' | 'down' }): Promise<void> {
281
const editorService = accessor.get(IEditorService);
282
283
const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane);
284
if (!editor) {
285
return;
286
}
287
288
const levels = args && args.levels || 1;
289
const direction = args && args.direction === 'up' ? 'up' : 'down';
290
let index: number | undefined = undefined;
291
292
if (args) {
293
index = args.index;
294
} else {
295
const activeCell = editor.getActiveCell();
296
if (!activeCell) {
297
return;
298
}
299
index = editor.getCellIndex(activeCell);
300
}
301
302
const controller = editor.getContribution<FoldingController>(FoldingController.id);
303
if (index !== undefined) {
304
if (direction === 'up') {
305
controller.setFoldingStateUp(index, CellFoldingState.Expanded, levels);
306
} else {
307
controller.setFoldingStateDown(index, CellFoldingState.Expanded, levels);
308
}
309
}
310
}
311
});
312
313