Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/simulation/fixtures/fix/issue-7544/notebookMulticursor.ts
13405 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 { Emitter, Event } from 'vs/base/common/event';
7
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
8
import { DisposableStore } from 'vs/base/common/lifecycle';
9
import { ResourceMap } from 'vs/base/common/map';
10
import { EditorConfiguration } from 'vs/editor/browser/config/editorConfiguration';
11
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
12
import { RedoCommand, UndoCommand } from 'vs/editor/browser/editorExtensions';
13
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget';
14
import { IEditorConfiguration } from 'vs/editor/common/config/editorConfiguration';
15
import { Position } from 'vs/editor/common/core/position';
16
import { Range } from 'vs/editor/common/core/range';
17
import { Selection, SelectionDirection } from 'vs/editor/common/core/selection';
18
import { IWordAtPosition, USUAL_WORD_SEPARATORS } from 'vs/editor/common/core/wordHelper';
19
import { CursorsController } from 'vs/editor/common/cursor/cursor';
20
import { CursorConfiguration, ICursorSimpleModel } from 'vs/editor/common/cursorCommon';
21
import { CursorChangeReason } from 'vs/editor/common/cursorEvents';
22
import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry';
23
import { IModelDeltaDecoration, ITextModel, PositionAffinity } from 'vs/editor/common/model';
24
import { indentOfLine } from 'vs/editor/common/model/textModel';
25
import { ITextModelService } from 'vs/editor/common/services/resolverService';
26
import { ICoordinatesConverter } from 'vs/editor/common/viewModel';
27
import { ViewModelEventsCollector } from 'vs/editor/common/viewModelEventDispatcher';
28
import { localize } from 'vs/nls';
29
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
30
import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
31
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
32
import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
33
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
34
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
35
import { IPastFutureElements, IUndoRedoElement, IUndoRedoService, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo';
36
import { registerWorkbenchContribution2, WorkbenchPhase } from 'vs/workbench/common/contributions';
37
import { INotebookActionContext, NotebookAction } from 'vs/workbench/contrib/notebook/browser/controller/coreActions';
38
import { getNotebookEditorFromEditorPane, ICellViewModel, INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
39
import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions';
40
import { CellEditorOptions } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions';
41
import { NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/common/notebookContextKeys';
42
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
43
44
const NOTEBOOK_ADD_FIND_MATCH_TO_SELECTION_ID = 'notebook.addFindMatchToSelection';
45
46
export const NOTEBOOK_MULTI_SELECTION_CONTEXT = {
47
IsNotebookMultiSelect: new RawContextKey<boolean>('isNotebookMultiSelect', false),
48
};
49
50
enum NotebookMultiCursorState {
51
Idle,
52
Selecting,
53
Editing,
54
}
55
56
interface TrackedMatch {
57
cellViewModel: ICellViewModel;
58
selections: Selection[];
59
config: IEditorConfiguration;
60
decorationIds: string[];
61
elements: IPastFutureElements;
62
}
63
64
class Disposable {
65
protected _register<T>(t: T): T {
66
return t;
67
}
68
69
protected dispose() { }
70
}
71
72
73
export class NotebookMultiCursorController extends Disposable implements INotebookEditorContribution {
74
75
76
static readonly id: string = 'notebook.multiCursorController';
77
78
private state: NotebookMultiCursorState = NotebookMultiCursorState.Idle;
79
80
private word: string = '';
81
private trackedMatches: TrackedMatch[] = [];
82
83
private readonly _onDidChangeAnchorCell = this._register(new Emitter<void>());
84
readonly onDidChangeAnchorCell: Event<void> = this._onDidChangeAnchorCell.event;
85
private anchorCell: [ICellViewModel, ICodeEditor] | undefined;
86
87
private readonly anchorDisposables = this._register(new DisposableStore());
88
private readonly cursorsDisposables = this._register(new DisposableStore());
89
private cursorsControllers: ResourceMap<[ITextModel, CursorsController]> = new ResourceMap<[ITextModel, CursorsController]>();
90
91
constructor(
92
private readonly notebookEditor: INotebookEditor,
93
private readonly contextKeyService: IContextKeyService,
94
private readonly textModelService: ITextModelService,
95
private readonly languageConfigurationService: ILanguageConfigurationService,
96
private readonly accessibilityService: IAccessibilityService,
97
private readonly configurationService: IConfigurationService,
98
private readonly undoRedoService: IUndoRedoService,
99
) {
100
super();
101
102
if (!this.configurationService.getValue<boolean>('notebook.multiSelect.enabled')) {
103
return;
104
}
105
106
this.anchorCell = this.notebookEditor.activeCellAndCodeEditor;
107
108
// anchor cell will catch and relay all type, cut, paste events to the cursors controllers
109
// need to create new controllers when the anchor cell changes, then update their listeners
110
// ** cursor controllers need to happen first, because anchor listeners relay to them
111
this._register(this.onDidChangeAnchorCell(() => {
112
this.updateCursorsControllers();
113
this.updateAnchorListeners();
114
}));
115
}
116
117
private updateCursorsControllers() {
118
this.cursorsDisposables.clear();
119
this.trackedMatches.forEach(async match => {
120
// skip this for the anchor cell, there is already a controller for it since it's the focused editor
121
if (match.cellViewModel.handle === this.anchorCell?.[0].handle) {
122
return;
123
}
124
125
const textModelRef = await this.textModelService.createModelReference(match.cellViewModel.uri);
126
const textModel = textModelRef.object.textEditorModel;
127
if (!textModel) {
128
return;
129
}
130
131
const editorConfig = match.config;
132
133
const converter = this.constructCoordinatesConverter();
134
const cursorSimpleModel = this.constructCursorSimpleModel(match.cellViewModel);
135
const controller = this.cursorsDisposables.add(new CursorsController(
136
textModel,
137
cursorSimpleModel,
138
converter,
139
new CursorConfiguration(textModel.getLanguageId(), textModel.getOptions(), editorConfig, this.languageConfigurationService)
140
));
141
controller.setSelections(new ViewModelEventsCollector(), undefined, match.selections, CursorChangeReason.Explicit);
142
this.cursorsControllers.set(match.cellViewModel.uri, [textModel, controller]);
143
});
144
}
145
146
private constructCoordinatesConverter(): ICoordinatesConverter {
147
return {
148
convertViewPositionToModelPosition(viewPosition: Position): Position {
149
return viewPosition;
150
},
151
convertViewRangeToModelRange(viewRange: Range): Range {
152
return viewRange;
153
},
154
validateViewPosition(viewPosition: Position, expectedModelPosition: Position): Position {
155
return viewPosition;
156
},
157
validateViewRange(viewRange: Range, expectedModelRange: Range): Range {
158
return viewRange;
159
},
160
convertModelPositionToViewPosition(modelPosition: Position, affinity?: PositionAffinity, allowZeroLineNumber?: boolean, belowHiddenRanges?: boolean): Position {
161
return modelPosition;
162
},
163
convertModelRangeToViewRange(modelRange: Range, affinity?: PositionAffinity): Range {
164
return modelRange;
165
},
166
modelPositionIsVisible(modelPosition: Position): boolean {
167
return true;
168
},
169
getModelLineViewLineCount(modelLineNumber: number): number {
170
return 1;
171
},
172
getViewLineNumberOfModelPosition(modelLineNumber: number, modelColumn: number): number {
173
return modelLineNumber;
174
}
175
};
176
}
177
178
private constructCursorSimpleModel(cell: ICellViewModel): ICursorSimpleModel {
179
return {
180
getLineCount(): number {
181
return cell.textBuffer.getLineCount();
182
},
183
getLineContent(lineNumber: number): string {
184
return cell.textBuffer.getLineContent(lineNumber);
185
},
186
getLineMinColumn(lineNumber: number): number {
187
return cell.textBuffer.getLineMinColumn(lineNumber);
188
},
189
getLineMaxColumn(lineNumber: number): number {
190
return cell.textBuffer.getLineMaxColumn(lineNumber);
191
},
192
getLineFirstNonWhitespaceColumn(lineNumber: number): number {
193
return cell.textBuffer.getLineFirstNonWhitespaceColumn(lineNumber);
194
},
195
getLineLastNonWhitespaceColumn(lineNumber: number): number {
196
return cell.textBuffer.getLineLastNonWhitespaceColumn(lineNumber);
197
},
198
normalizePosition(position: Position, affinity: PositionAffinity): Position {
199
return position;
200
},
201
getLineIndentColumn(lineNumber: number): number {
202
return indentOfLine(cell.textBuffer.getLineContent(lineNumber)) + 1;
203
}
204
};
205
}
206
207
private updateAnchorListeners() {
208
this.anchorDisposables.clear();
209
210
if (!this.anchorCell) {
211
throw new Error('Anchor cell is undefined');
212
}
213
214
// typing
215
this.anchorDisposables.add(this.anchorCell[1].onWillType((input) => {
216
this.state = NotebookMultiCursorState.Editing; // typing will continue to work as normal across ranges, just preps for another cmd+d
217
this.cursorsControllers.forEach(cursorController => {
218
cursorController[1].type(new ViewModelEventsCollector(), input, 'keyboard');
219
220
});
221
}));
222
223
this.anchorDisposables.add(this.anchorCell[1].onDidType(() => {
224
this.state = NotebookMultiCursorState.Idle;
225
this.updateLazyDecorations();
226
}));
227
228
// exit mode
229
this.anchorDisposables.add(this.anchorCell[1].onDidChangeCursorSelection((e) => {
230
if (e.source === 'mouse' || e.source === 'deleteLeft' || e.source === 'deleteRight') {
231
this.resetToIdleState();
232
}
233
}));
234
235
this.anchorDisposables.add(this.anchorCell[1].onDidBlurEditorWidget(() => {
236
if (this.state === NotebookMultiCursorState.Editing || this.state === NotebookMultiCursorState.Selecting) {
237
this.resetToIdleState();
238
}
239
}));
240
}
241
242
private updateFinalUndoRedo() {
243
const anchorCellModel = this.anchorCell?.[1].getModel();
244
if (!anchorCellModel) {
245
// should not happen
246
return;
247
}
248
249
const textModels = [anchorCellModel];
250
this.cursorsControllers.forEach(controller => {
251
const model = controller[0];
252
textModels.push(model);
253
});
254
255
const newElementsMap: ResourceMap<IUndoRedoElement[]> = new ResourceMap<IUndoRedoElement[]>();
256
257
textModels.forEach(model => {
258
const trackedMatch = this.trackedMatches.find(match => match.cellViewModel.uri.toString() === model.uri.toString());
259
if (!trackedMatch) {
260
return;
261
}
262
const undoRedoState = trackedMatch.undoRedo;
263
if (!undoRedoState) {
264
return;
265
}
266
267
const currentPastElements = this.undoRedoService.getElements(model.uri).past.slice();
268
const oldPastElements = trackedMatch.undoRedo.elements.past.slice();
269
const newElements = currentPastElements.slice(oldPastElements.length);
270
if (newElements.length === 0) {
271
return;
272
}
273
274
newElementsMap.set(model.uri, newElements);
275
276
this.undoRedoService.removeElements(model.uri);
277
oldPastElements.forEach(element => {
278
this.undoRedoService.pushElement(element);
279
});
280
});
281
282
this.undoRedoService.pushElement({
283
type: UndoRedoElementType.Workspace,
284
resources: textModels.map(model => model.uri),
285
label: 'Multi Cursor Edit',
286
code: 'multiCursorEdit',
287
confirmBeforeUndo: false,
288
undo: async () => {
289
newElementsMap.forEach(async value => {
290
value.reverse().forEach(async element => {
291
await element.undo();
292
});
293
});
294
},
295
redo: async () => {
296
newElementsMap.forEach(async value => {
297
value.forEach(async element => {
298
await element.redo();
299
});
300
});
301
}
302
});
303
}
304
305
public resetToIdleState() {
306
this.state = NotebookMultiCursorState.Idle;
307
this.updateFinalUndoRedo();
308
309
this.trackedMatches.forEach(match => {
310
this.clearDecorations(match);
311
});
312
313
// todo: polish -- store the precise first selection the user makes. this just sets to the end of the word (due to idle->selecting state transition logic)
314
this.trackedMatches[0].cellViewModel.setSelections([this.trackedMatches[0].selections[0]]);
315
316
this.anchorDisposables.clear();
317
this.cursorsDisposables.clear();
318
this.cursorsControllers.clear();
319
this.trackedMatches = [];
320
}
321
322
public async findAndTrackNextSelection(cell: ICellViewModel): Promise<void> {
323
if (this.state === NotebookMultiCursorState.Idle) { // move cursor to end of the symbol + track it, transition to selecting state
324
const textModel = cell.textModel;
325
if (!textModel) {
326
return;
327
}
328
329
const inputSelection = cell.getSelections()[0];
330
const word = this.getWord(inputSelection, textModel);
331
if (!word) {
332
return;
333
}
334
this.word = word.word;
335
336
const newSelection = new Selection(
337
inputSelection.startLineNumber,
338
word.startColumn,
339
inputSelection.startLineNumber,
340
word.endColumn
341
);
342
cell.setSelections([newSelection]);
343
344
this.anchorCell = this.notebookEditor.activeCellAndCodeEditor;
345
if (!this.anchorCell || this.anchorCell[0].handle !== cell.handle) {
346
throw new Error('Active cell is not the same as the cell passed as context');
347
}
348
if (!(this.anchorCell[1] instanceof CodeEditorWidget)) {
349
throw new Error('Active cell is not an instance of CodeEditorWidget');
350
}
351
352
textModel.pushStackElement();
353
354
this.trackedMatches = [];
355
const editorConfig = this.constructCellEditorOptions(this.anchorCell[0]);
356
const newMatch: TrackedMatch = {
357
cellViewModel: cell,
358
selections: [newSelection],
359
config: editorConfig, // cache this in the match so we can create new cursors controllers with the correct language config
360
decorationIds: [],
361
undoRedo: {
362
elements: this.undoRedoService.getElements(cell.uri),
363
}
364
};
365
this.trackedMatches.push(newMatch);
366
367
this.initializeMultiSelectDecorations(newMatch);
368
this.state = NotebookMultiCursorState.Selecting;
369
this._onDidChangeAnchorCell.fire();
370
371
} else if (this.state === NotebookMultiCursorState.Selecting) { // use the word we stored from idle state transition to find next match, track it
372
const notebookTextModel = this.notebookEditor.textModel;
373
if (!notebookTextModel) {
374
return;
375
}
376
377
const index = this.notebookEditor.getCellIndex(cell);
378
if (index === undefined) {
379
return;
380
}
381
382
const findResult = notebookTextModel.findNextMatch(
383
this.word,
384
{ cellIndex: index, position: cell.getSelections()[cell.getSelections().length - 1].getEndPosition() },
385
false,
386
true,
387
USUAL_WORD_SEPARATORS //! might want to get these from the editor config
388
);
389
if (!findResult) {
390
return; //todo: some sort of message to the user alerting them that there are no more matches? editor does not do this
391
}
392
393
const resultCellViewModel = this.notebookEditor.getCellByHandle(findResult.cell.handle);
394
if (!resultCellViewModel) {
395
return;
396
}
397
398
let newMatch: TrackedMatch;
399
if (findResult.cell.handle !== cell.handle) { // result is in a different cell, move focus there and apply selection, then update anchor
400
await this.notebookEditor.revealRangeInViewAsync(resultCellViewModel, findResult.match.range);
401
this.notebookEditor.focusNotebookCell(resultCellViewModel, 'editor');
402
403
const newSelection = Selection.fromRange(findResult.match.range, SelectionDirection.LTR);
404
resultCellViewModel.setSelections([newSelection]);
405
406
this.anchorCell = this.notebookEditor.activeCellAndCodeEditor;
407
if (!this.anchorCell || !(this.anchorCell[1] instanceof CodeEditorWidget)) {
408
throw new Error('Active cell is not an instance of CodeEditorWidget');
409
}
410
411
const textModel = await resultCellViewModel.resolveTextModel();
412
textModel.pushStackElement();
413
414
newMatch = {
415
cellViewModel: resultCellViewModel,
416
selections: [newSelection],
417
config: this.constructCellEditorOptions(this.anchorCell[0]),
418
decorationIds: [],
419
undoRedo: {
420
elements: this.undoRedoService.getElements(resultCellViewModel.uri),
421
}
422
} satisfies TrackedMatch;
423
this.trackedMatches.push(newMatch);
424
425
this._onDidChangeAnchorCell.fire();
426
427
} else { // match is in the same cell, find tracked entry, update and set selections
428
newMatch = this.trackedMatches.find(match => match.cellViewModel.handle === findResult.cell.handle)!;
429
newMatch.selections.push(Selection.fromRange(findResult.match.range, SelectionDirection.LTR));
430
resultCellViewModel.setSelections(newMatch.selections);
431
}
432
433
this.initializeMultiSelectDecorations(newMatch);
434
}
435
}
436
437
private constructCellEditorOptions(cell: ICellViewModel): EditorConfiguration {
438
const cellEditorOptions = new CellEditorOptions(this.notebookEditor.getBaseCellEditorOptions(cell.language), this.notebookEditor.notebookOptions, this.configurationService);
439
const options = cellEditorOptions.getUpdatedValue(cell.internalMetadata, cell.uri);
440
return new EditorConfiguration(false, MenuId.EditorContent, options, null, this.accessibilityService);
441
}
442
443
/**
444
* Updates the multicursor selection decorations for a specific matched cell
445
*
446
* @param match -- match object containing the viewmodel + selections
447
*/
448
private initializeMultiSelectDecorations(match: TrackedMatch) {
449
const decorations: IModelDeltaDecoration[] = [];
450
451
match.selections.forEach(selection => {
452
decorations.push({
453
range: selection,
454
options: {
455
description: '',
456
className: 'nb-multicursor-selection',
457
}
458
});
459
});
460
461
match.decorationIds = match.cellViewModel.deltaModelDecorations(
462
match.decorationIds,
463
decorations
464
);
465
}
466
467
private updateLazyDecorations() {
468
// const visibleRange = this.notebookEditor.visibleRanges;
469
470
// for every tracked match that is not in the visible range, dispose of their decorations and update them based off the cursorcontroller
471
this.trackedMatches.forEach(match => {
472
const cellIndex = this.notebookEditor.getCellIndex(match.cellViewModel);
473
if (cellIndex === undefined) {
474
return;
475
}
476
477
let selections;
478
const controller = this.cursorsControllers.get(match.cellViewModel.uri);
479
if (!controller) { // active cell doesn't get a stored controller from us
480
selections = this.notebookEditor.activeCodeEditor?.getSelections();
481
} else {
482
selections = controller[1].getSelections();
483
}
484
485
const newDecorations = selections?.map(selection => {
486
return {
487
range: selection,
488
options: {
489
description: '',
490
className: 'nb-multicursor-selection',
491
}
492
};
493
});
494
495
match.decorationIds = match.cellViewModel.deltaModelDecorations(
496
match.decorationIds,
497
newDecorations ?? []
498
);
499
});
500
}
501
502
private clearDecorations(match: TrackedMatch) {
503
match.decorationIds = match.cellViewModel.deltaModelDecorations(
504
match.decorationIds,
505
[]
506
);
507
}
508
509
async undo() {
510
const anchorCellModel = this.anchorCell?.[1].getModel();
511
if (!anchorCellModel) {
512
// should not happen
513
return;
514
}
515
516
const models = [anchorCellModel];
517
this.cursorsControllers.forEach(controller => {
518
const model = controller[0];
519
models.push(model);
520
});
521
522
await Promise.all(models.map(model => model.undo()));
523
}
524
525
async redo() {
526
const anchorCellModel = this.anchorCell?.[1].getModel();
527
if (!anchorCellModel) {
528
// should not happen
529
return;
530
}
531
532
const models = [anchorCellModel];
533
this.cursorsControllers.forEach(controller => {
534
const model = controller[0];
535
models.push(model);
536
});
537
538
await Promise.all(models.map(model => model.redo()));
539
}
540
541
private getWord(selection: Selection, model: ITextModel): IWordAtPosition | null {
542
const lineNumber = selection.startLineNumber;
543
const startColumn = selection.startColumn;
544
545
if (model.isDisposed()) {
546
return null;
547
}
548
549
return model.getWordAtPosition({
550
lineNumber: lineNumber,
551
column: startColumn
552
});
553
}
554
555
override dispose(): void {
556
super.dispose();
557
this.anchorDisposables.dispose();
558
this.cursorsDisposables.dispose();
559
560
this.trackedMatches.forEach(match => {
561
this.clearDecorations(match);
562
});
563
this.trackedMatches = [];
564
}
565
566
}
567
568
class NotebookAddMatchToMultiSelectionAction extends NotebookAction {
569
constructor() {
570
super({
571
id: NOTEBOOK_ADD_FIND_MATCH_TO_SELECTION_ID,
572
title: localize('addFindMatchToSelection', "Add Find Match to Selection"),
573
keybinding: {
574
when: ContextKeyExpr.and(
575
ContextKeyExpr.equals('config.notebook.multiSelect.enabled', true),
576
NOTEBOOK_IS_ACTIVE_EDITOR,
577
NOTEBOOK_CELL_EDITOR_FOCUSED,
578
),
579
primary: KeyMod.CtrlCmd | KeyCode.KeyD,
580
weight: KeybindingWeight.WorkbenchContrib
581
}
582
});
583
}
584
585
async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise<void> {
586
const editorService = accessor.get(IEditorService);
587
const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane);
588
589
if (!editor) {
590
return;
591
}
592
593
if (!context.cell) {
594
return;
595
}
596
597
const controller = editor.getContribution<NotebookMultiCursorController>(NotebookMultiCursorController.id);
598
controller.findAndTrackNextSelection(context.cell);
599
}
600
}
601
602
class NotebookExitMultiSelectionAction extends NotebookAction {
603
constructor() {
604
super({
605
id: 'noteMultiCursor.exit',
606
title: localize('exitMultiSelection', "Exit Multi Cursor Mode"),
607
keybinding: {
608
when: ContextKeyExpr.and(
609
ContextKeyExpr.equals('config.notebook.multiSelect.enabled', true),
610
NOTEBOOK_IS_ACTIVE_EDITOR,
611
NOTEBOOK_MULTI_SELECTION_CONTEXT.IsNotebookMultiSelect,
612
),
613
primary: KeyCode.Escape,
614
weight: KeybindingWeight.WorkbenchContrib
615
}
616
});
617
}
618
619
async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext): Promise<void> {
620
const editorService = accessor.get(IEditorService);
621
const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane);
622
623
if (!editor) {
624
return;
625
}
626
627
const controller = editor.getContribution<NotebookMultiCursorController>(NotebookMultiCursorController.id);
628
controller.resetToIdleState();
629
}
630
}
631
632
class NotebookMultiCursorUndoRedoContribution extends Disposable {
633
634
static readonly ID = 'workbench.contrib.notebook.multiCursorUndoRedo';
635
636
constructor(private readonly _editorService: IEditorService) {
637
super();
638
639
const PRIORITY = 10005;
640
this._register(UndoCommand.addImplementation(PRIORITY, 'notebook-multicursor-undo-redo', () => {
641
const editor = getNotebookEditorFromEditorPane(this._editorService.activeEditorPane);
642
if (!editor) {
643
return false;
644
}
645
646
if (!editor.hasModel()) {
647
return false;
648
}
649
650
const controller = editor.getContribution<NotebookMultiCursorController>(NotebookMultiCursorController.id);
651
652
return controller.undo();
653
}, ContextKeyExpr.and(
654
ContextKeyExpr.equals('config.notebook.multiSelect.enabled', true),
655
NOTEBOOK_IS_ACTIVE_EDITOR,
656
NOTEBOOK_MULTI_SELECTION_CONTEXT.IsNotebookMultiSelect,
657
)));
658
659
this._register(RedoCommand.addImplementation(PRIORITY, 'notebook-multicursor-undo-redo', () => {
660
const editor = getNotebookEditorFromEditorPane(this._editorService.activeEditorPane);
661
if (!editor) {
662
return false;
663
}
664
665
if (!editor.hasModel()) {
666
return false;
667
}
668
669
const controller = editor.getContribution<NotebookMultiCursorController>(NotebookMultiCursorController.id);
670
return controller.redo();
671
}, ContextKeyExpr.and(
672
ContextKeyExpr.equals('config.notebook.multiSelect.enabled', true),
673
NOTEBOOK_IS_ACTIVE_EDITOR,
674
NOTEBOOK_MULTI_SELECTION_CONTEXT.IsNotebookMultiSelect,
675
)));
676
}
677
}
678
679
registerNotebookContribution(NotebookMultiCursorController.id, NotebookMultiCursorController);
680
registerAction2(NotebookAddMatchToMultiSelectionAction);
681
registerAction2(NotebookExitMultiSelectionAction);
682
registerWorkbenchContribution2(NotebookMultiCursorUndoRedoContribution.ID, NotebookMultiCursorUndoRedoContribution, WorkbenchPhase.BlockRestore);
683
684