Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts
5220 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 { KeyChord, KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
7
import * as nls from '../../../../nls.js';
8
import { MenuId } from '../../../../platform/actions/common/actions.js';
9
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
10
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
11
import { CoreEditingCommands } from '../../../browser/coreCommands.js';
12
import { IActiveCodeEditor, ICodeEditor } from '../../../browser/editorBrowser.js';
13
import { EditorAction, IActionOptions, registerEditorAction, ServicesAccessor } from '../../../browser/editorExtensions.js';
14
import { ReplaceCommand, ReplaceCommandThatPreservesSelection, ReplaceCommandThatSelectsText } from '../../../common/commands/replaceCommand.js';
15
import { TrimTrailingWhitespaceCommand } from '../../../common/commands/trimTrailingWhitespaceCommand.js';
16
import { EditorOption } from '../../../common/config/editorOptions.js';
17
import { EditOperation, ISingleEditOperation } from '../../../common/core/editOperation.js';
18
import { Position } from '../../../common/core/position.js';
19
import { Range } from '../../../common/core/range.js';
20
import { Selection } from '../../../common/core/selection.js';
21
import { EnterOperation } from '../../../common/cursor/cursorTypeEditOperations.js';
22
import { TypeOperations } from '../../../common/cursor/cursorTypeOperations.js';
23
import { ICommand } from '../../../common/editorCommon.js';
24
import { EditorContextKeys } from '../../../common/editorContextKeys.js';
25
import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js';
26
import { ITextModel } from '../../../common/model.js';
27
import { CopyLinesCommand } from './copyLinesCommand.js';
28
import { MoveLinesCommand } from './moveLinesCommand.js';
29
import { SortLinesCommand } from './sortLinesCommand.js';
30
31
// copy lines
32
33
abstract class AbstractCopyLinesAction extends EditorAction {
34
35
private readonly down: boolean;
36
37
constructor(down: boolean, opts: IActionOptions) {
38
super(opts);
39
this.down = down;
40
}
41
42
public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {
43
if (!editor.hasModel()) {
44
return;
45
}
46
47
const selections = editor.getSelections().map((selection, index) => ({ selection, index, ignore: false }));
48
selections.sort((a, b) => Range.compareRangesUsingStarts(a.selection, b.selection));
49
50
// Remove selections that would result in copying the same line
51
let prev = selections[0];
52
for (let i = 1; i < selections.length; i++) {
53
const curr = selections[i];
54
if (prev.selection.endLineNumber === curr.selection.startLineNumber) {
55
// these two selections would copy the same line
56
if (prev.index < curr.index) {
57
// prev wins
58
curr.ignore = true;
59
} else {
60
// curr wins
61
prev.ignore = true;
62
prev = curr;
63
}
64
}
65
}
66
67
const commands: ICommand[] = [];
68
for (const selection of selections) {
69
commands.push(new CopyLinesCommand(selection.selection, this.down, selection.ignore));
70
}
71
72
editor.pushUndoStop();
73
editor.executeCommands(this.id, commands);
74
editor.pushUndoStop();
75
}
76
}
77
78
class CopyLinesUpAction extends AbstractCopyLinesAction {
79
constructor() {
80
super(false, {
81
id: 'editor.action.copyLinesUpAction',
82
label: nls.localize2('lines.copyUp', "Copy Line Up"),
83
precondition: EditorContextKeys.writable,
84
kbOpts: {
85
kbExpr: EditorContextKeys.editorTextFocus,
86
primary: KeyMod.Alt | KeyMod.Shift | KeyCode.UpArrow,
87
linux: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.UpArrow },
88
weight: KeybindingWeight.EditorContrib
89
},
90
menuOpts: {
91
menuId: MenuId.MenubarSelectionMenu,
92
group: '2_line',
93
title: nls.localize({ key: 'miCopyLinesUp', comment: ['&& denotes a mnemonic'] }, "&&Copy Line Up"),
94
order: 1
95
},
96
canTriggerInlineEdits: true,
97
});
98
}
99
}
100
101
class CopyLinesDownAction extends AbstractCopyLinesAction {
102
constructor() {
103
super(true, {
104
id: 'editor.action.copyLinesDownAction',
105
label: nls.localize2('lines.copyDown', "Copy Line Down"),
106
precondition: EditorContextKeys.writable,
107
kbOpts: {
108
kbExpr: EditorContextKeys.editorTextFocus,
109
primary: KeyMod.Alt | KeyMod.Shift | KeyCode.DownArrow,
110
linux: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.DownArrow },
111
weight: KeybindingWeight.EditorContrib
112
},
113
menuOpts: {
114
menuId: MenuId.MenubarSelectionMenu,
115
group: '2_line',
116
title: nls.localize({ key: 'miCopyLinesDown', comment: ['&& denotes a mnemonic'] }, "Co&&py Line Down"),
117
order: 2
118
},
119
canTriggerInlineEdits: true,
120
});
121
}
122
}
123
124
export class DuplicateSelectionAction extends EditorAction {
125
126
constructor() {
127
super({
128
id: 'editor.action.duplicateSelection',
129
label: nls.localize2('duplicateSelection', "Duplicate Selection"),
130
precondition: EditorContextKeys.writable,
131
menuOpts: {
132
menuId: MenuId.MenubarSelectionMenu,
133
group: '2_line',
134
title: nls.localize({ key: 'miDuplicateSelection', comment: ['&& denotes a mnemonic'] }, "&&Duplicate Selection"),
135
order: 5
136
},
137
canTriggerInlineEdits: true,
138
});
139
}
140
141
public run(accessor: ServicesAccessor, editor: ICodeEditor, args: unknown): void {
142
if (!editor.hasModel()) {
143
return;
144
}
145
146
const commands: ICommand[] = [];
147
const selections = editor.getSelections();
148
const model = editor.getModel();
149
150
for (const selection of selections) {
151
if (selection.isEmpty()) {
152
commands.push(new CopyLinesCommand(selection, true));
153
} else {
154
const insertSelection = new Selection(selection.endLineNumber, selection.endColumn, selection.endLineNumber, selection.endColumn);
155
commands.push(new ReplaceCommandThatSelectsText(insertSelection, model.getValueInRange(selection)));
156
}
157
}
158
159
editor.pushUndoStop();
160
editor.executeCommands(this.id, commands);
161
editor.pushUndoStop();
162
}
163
}
164
165
// move lines
166
167
abstract class AbstractMoveLinesAction extends EditorAction {
168
169
private readonly down: boolean;
170
171
constructor(down: boolean, opts: IActionOptions) {
172
super(opts);
173
this.down = down;
174
}
175
176
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
177
const languageConfigurationService = accessor.get(ILanguageConfigurationService);
178
179
const commands: ICommand[] = [];
180
const selections = editor.getSelections() || [];
181
const autoIndent = editor.getOption(EditorOption.autoIndent);
182
183
for (const selection of selections) {
184
commands.push(new MoveLinesCommand(selection, this.down, autoIndent, languageConfigurationService));
185
}
186
187
editor.pushUndoStop();
188
editor.executeCommands(this.id, commands);
189
editor.pushUndoStop();
190
}
191
}
192
193
class MoveLinesUpAction extends AbstractMoveLinesAction {
194
constructor() {
195
super(false, {
196
id: 'editor.action.moveLinesUpAction',
197
label: nls.localize2('lines.moveUp', "Move Line Up"),
198
precondition: EditorContextKeys.writable,
199
kbOpts: {
200
kbExpr: EditorContextKeys.editorTextFocus,
201
primary: KeyMod.Alt | KeyCode.UpArrow,
202
linux: { primary: KeyMod.Alt | KeyCode.UpArrow },
203
weight: KeybindingWeight.EditorContrib
204
},
205
menuOpts: {
206
menuId: MenuId.MenubarSelectionMenu,
207
group: '2_line',
208
title: nls.localize({ key: 'miMoveLinesUp', comment: ['&& denotes a mnemonic'] }, "Mo&&ve Line Up"),
209
order: 3
210
},
211
canTriggerInlineEdits: true,
212
});
213
}
214
}
215
216
class MoveLinesDownAction extends AbstractMoveLinesAction {
217
constructor() {
218
super(true, {
219
id: 'editor.action.moveLinesDownAction',
220
label: nls.localize2('lines.moveDown', "Move Line Down"),
221
precondition: EditorContextKeys.writable,
222
kbOpts: {
223
kbExpr: EditorContextKeys.editorTextFocus,
224
primary: KeyMod.Alt | KeyCode.DownArrow,
225
linux: { primary: KeyMod.Alt | KeyCode.DownArrow },
226
weight: KeybindingWeight.EditorContrib
227
},
228
menuOpts: {
229
menuId: MenuId.MenubarSelectionMenu,
230
group: '2_line',
231
title: nls.localize({ key: 'miMoveLinesDown', comment: ['&& denotes a mnemonic'] }, "Move &&Line Down"),
232
order: 4
233
},
234
canTriggerInlineEdits: true,
235
});
236
}
237
}
238
239
export abstract class AbstractSortLinesAction extends EditorAction {
240
private readonly descending: boolean;
241
242
constructor(descending: boolean, opts: IActionOptions) {
243
super(opts);
244
this.descending = descending;
245
}
246
247
public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {
248
if (!editor.hasModel()) {
249
return;
250
}
251
252
const model = editor.getModel();
253
let selections = editor.getSelections();
254
if (selections.length === 1 && selections[0].isSingleLine()) {
255
// Apply to whole document.
256
selections = [new Selection(1, 1, model.getLineCount(), model.getLineMaxColumn(model.getLineCount()))];
257
}
258
259
for (const selection of selections) {
260
if (!SortLinesCommand.canRun(editor.getModel(), selection, this.descending)) {
261
return;
262
}
263
}
264
265
const commands: ICommand[] = [];
266
for (let i = 0, len = selections.length; i < len; i++) {
267
commands[i] = new SortLinesCommand(selections[i], this.descending);
268
}
269
270
editor.pushUndoStop();
271
editor.executeCommands(this.id, commands);
272
editor.pushUndoStop();
273
}
274
}
275
276
export class SortLinesAscendingAction extends AbstractSortLinesAction {
277
constructor() {
278
super(false, {
279
id: 'editor.action.sortLinesAscending',
280
label: nls.localize2('lines.sortAscending', "Sort Lines Ascending"),
281
precondition: EditorContextKeys.writable,
282
canTriggerInlineEdits: true,
283
});
284
}
285
}
286
287
export class SortLinesDescendingAction extends AbstractSortLinesAction {
288
constructor() {
289
super(true, {
290
id: 'editor.action.sortLinesDescending',
291
label: nls.localize2('lines.sortDescending', "Sort Lines Descending"),
292
precondition: EditorContextKeys.writable,
293
canTriggerInlineEdits: true,
294
});
295
}
296
}
297
298
export class DeleteDuplicateLinesAction extends EditorAction {
299
constructor() {
300
super({
301
id: 'editor.action.removeDuplicateLines',
302
label: nls.localize2('lines.deleteDuplicates', "Delete Duplicate Lines"),
303
precondition: EditorContextKeys.writable,
304
canTriggerInlineEdits: true,
305
});
306
}
307
308
public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {
309
if (!editor.hasModel()) {
310
return;
311
}
312
313
const model: ITextModel = editor.getModel();
314
if (model.getLineCount() === 1 && model.getLineMaxColumn(1) === 1) {
315
return;
316
}
317
318
const edits: ISingleEditOperation[] = [];
319
const endCursorState: Selection[] = [];
320
321
let linesDeleted = 0;
322
let updateSelection = true;
323
324
let selections = editor.getSelections();
325
if (selections.length === 1 && selections[0].isSingleLine()) {
326
// Apply to whole document.
327
selections = [new Selection(1, 1, model.getLineCount(), model.getLineMaxColumn(model.getLineCount()))];
328
updateSelection = false;
329
}
330
331
for (const selection of selections) {
332
const uniqueLines = new Set();
333
const lines = [];
334
335
for (let i = selection.startLineNumber; i <= selection.endLineNumber; i++) {
336
const line = model.getLineContent(i);
337
338
if (uniqueLines.has(line)) {
339
continue;
340
}
341
342
lines.push(line);
343
uniqueLines.add(line);
344
}
345
346
347
const selectionToReplace = new Selection(
348
selection.startLineNumber,
349
1,
350
selection.endLineNumber,
351
model.getLineMaxColumn(selection.endLineNumber)
352
);
353
354
const adjustedSelectionStart = selection.startLineNumber - linesDeleted;
355
const finalSelection = new Selection(
356
adjustedSelectionStart,
357
1,
358
adjustedSelectionStart + lines.length - 1,
359
lines[lines.length - 1].length + 1
360
);
361
362
edits.push(EditOperation.replace(selectionToReplace, lines.join('\n')));
363
endCursorState.push(finalSelection);
364
365
linesDeleted += (selection.endLineNumber - selection.startLineNumber + 1) - lines.length;
366
}
367
368
editor.pushUndoStop();
369
editor.executeEdits(this.id, edits, updateSelection ? endCursorState : undefined);
370
editor.pushUndoStop();
371
}
372
}
373
374
export class ReverseLinesAction extends EditorAction {
375
constructor() {
376
super({
377
id: 'editor.action.reverseLines',
378
label: nls.localize2('lines.reverseLines', "Reverse lines"),
379
precondition: EditorContextKeys.writable,
380
canTriggerInlineEdits: true
381
});
382
}
383
384
public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {
385
if (!editor.hasModel()) {
386
return;
387
}
388
389
const model: ITextModel = editor.getModel();
390
const originalSelections = editor.getSelections();
391
let selections = originalSelections;
392
if (selections.length === 1 && selections[0].isSingleLine()) {
393
// Apply to whole document.
394
selections = [new Selection(1, 1, model.getLineCount(), model.getLineMaxColumn(model.getLineCount()))];
395
}
396
397
const edits: ISingleEditOperation[] = [];
398
const resultingSelections: Selection[] = [];
399
400
for (let i = 0; i < selections.length; i++) {
401
const selection = selections[i];
402
const originalSelection = originalSelections[i];
403
let endLineNumber = selection.endLineNumber;
404
if (selection.startLineNumber < selection.endLineNumber && selection.endColumn === 1) {
405
endLineNumber--;
406
}
407
408
let range: Range = new Range(selection.startLineNumber, 1, endLineNumber, model.getLineMaxColumn(endLineNumber));
409
410
// Exclude last line if empty and we're at the end of the document
411
if (endLineNumber === model.getLineCount() && model.getLineContent(range.endLineNumber) === '') {
412
range = range.setEndPosition(range.endLineNumber - 1, model.getLineMaxColumn(range.endLineNumber - 1));
413
}
414
415
const lines: string[] = [];
416
for (let i = range.endLineNumber; i >= range.startLineNumber; i--) {
417
lines.push(model.getLineContent(i));
418
}
419
const edit: ISingleEditOperation = EditOperation.replace(range, lines.join('\n'));
420
edits.push(edit);
421
422
const updateLineNumber = function (lineNumber: number): number {
423
return lineNumber <= range.endLineNumber ? range.endLineNumber - lineNumber + range.startLineNumber : lineNumber;
424
};
425
const updateSelection = function (sel: Selection): Selection {
426
if (sel.isEmpty()) {
427
// keep just the cursor
428
return new Selection(updateLineNumber(sel.positionLineNumber), sel.positionColumn, updateLineNumber(sel.positionLineNumber), sel.positionColumn);
429
} else {
430
// keep selection - maintain direction by creating backward selection
431
const newSelectionStart = updateLineNumber(sel.selectionStartLineNumber);
432
const newPosition = updateLineNumber(sel.positionLineNumber);
433
const newSelectionStartColumn = sel.selectionStartColumn;
434
const newPositionColumn = sel.positionColumn;
435
436
// Create selection: from (newSelectionStart, newSelectionStartColumn) to (newPosition, newPositionColumn)
437
// After reversal: from (3, 2) to (1, 3)
438
return new Selection(newSelectionStart, newSelectionStartColumn, newPosition, newPositionColumn);
439
}
440
};
441
resultingSelections.push(updateSelection(originalSelection));
442
}
443
444
editor.pushUndoStop();
445
editor.executeEdits(this.id, edits, resultingSelections);
446
editor.pushUndoStop();
447
}
448
}
449
450
interface TrimTrailingWhitespaceArgs {
451
reason?: 'auto-save';
452
}
453
454
export class TrimTrailingWhitespaceAction extends EditorAction {
455
456
public static readonly ID = 'editor.action.trimTrailingWhitespace';
457
458
constructor() {
459
super({
460
id: TrimTrailingWhitespaceAction.ID,
461
label: nls.localize2('lines.trimTrailingWhitespace', "Trim Trailing Whitespace"),
462
precondition: EditorContextKeys.writable,
463
kbOpts: {
464
kbExpr: EditorContextKeys.editorTextFocus,
465
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyX),
466
weight: KeybindingWeight.EditorContrib
467
}
468
});
469
}
470
471
public run(_accessor: ServicesAccessor, editor: ICodeEditor, args: TrimTrailingWhitespaceArgs): void {
472
473
let cursors: Position[] = [];
474
if (args.reason === 'auto-save') {
475
// See https://github.com/editorconfig/editorconfig-vscode/issues/47
476
// It is very convenient for the editor config extension to invoke this action.
477
// So, if we get a reason:'auto-save' passed in, let's preserve cursor positions.
478
cursors = (editor.getSelections() || []).map(s => new Position(s.positionLineNumber, s.positionColumn));
479
}
480
481
const selection = editor.getSelection();
482
if (selection === null) {
483
return;
484
}
485
486
const config = _accessor.get(IConfigurationService);
487
const model = editor.getModel();
488
const trimInRegexAndStrings = config.getValue<boolean>('files.trimTrailingWhitespaceInRegexAndStrings', { overrideIdentifier: model?.getLanguageId(), resource: model?.uri });
489
490
const command = new TrimTrailingWhitespaceCommand(selection, cursors, trimInRegexAndStrings);
491
492
editor.pushUndoStop();
493
editor.executeCommands(this.id, [command]);
494
editor.pushUndoStop();
495
}
496
}
497
498
// delete lines
499
500
interface IDeleteLinesOperation {
501
startLineNumber: number;
502
selectionStartColumn: number;
503
endLineNumber: number;
504
positionColumn: number;
505
}
506
507
export class DeleteLinesAction extends EditorAction {
508
509
constructor() {
510
super({
511
id: 'editor.action.deleteLines',
512
label: nls.localize2('lines.delete', "Delete Line"),
513
precondition: EditorContextKeys.writable,
514
kbOpts: {
515
kbExpr: EditorContextKeys.textInputFocus,
516
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyK,
517
weight: KeybindingWeight.EditorContrib
518
},
519
canTriggerInlineEdits: true,
520
});
521
}
522
523
public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {
524
if (!editor.hasModel()) {
525
return;
526
}
527
528
const ops = this._getLinesToRemove(editor);
529
530
const model: ITextModel = editor.getModel();
531
if (model.getLineCount() === 1 && model.getLineMaxColumn(1) === 1) {
532
// Model is empty
533
return;
534
}
535
536
let linesDeleted = 0;
537
const edits: ISingleEditOperation[] = [];
538
const cursorState: Selection[] = [];
539
for (let i = 0, len = ops.length; i < len; i++) {
540
const op = ops[i];
541
542
let startLineNumber = op.startLineNumber;
543
let endLineNumber = op.endLineNumber;
544
545
let startColumn = 1;
546
let endColumn = model.getLineMaxColumn(endLineNumber);
547
if (endLineNumber < model.getLineCount()) {
548
endLineNumber += 1;
549
endColumn = 1;
550
} else if (startLineNumber > 1) {
551
startLineNumber -= 1;
552
startColumn = model.getLineMaxColumn(startLineNumber);
553
}
554
555
edits.push(EditOperation.replace(new Selection(startLineNumber, startColumn, endLineNumber, endColumn), ''));
556
cursorState.push(new Selection(startLineNumber - linesDeleted, op.positionColumn, startLineNumber - linesDeleted, op.positionColumn));
557
linesDeleted += (op.endLineNumber - op.startLineNumber + 1);
558
}
559
560
editor.pushUndoStop();
561
editor.executeEdits(this.id, edits, cursorState);
562
editor.revealAllCursors(true);
563
editor.pushUndoStop();
564
}
565
566
private _getLinesToRemove(editor: IActiveCodeEditor): IDeleteLinesOperation[] {
567
// Construct delete operations
568
const operations: IDeleteLinesOperation[] = editor.getSelections().map((s) => {
569
570
let endLineNumber = s.endLineNumber;
571
if (s.startLineNumber < s.endLineNumber && s.endColumn === 1) {
572
endLineNumber -= 1;
573
}
574
575
return {
576
startLineNumber: s.startLineNumber,
577
selectionStartColumn: s.selectionStartColumn,
578
endLineNumber: endLineNumber,
579
positionColumn: s.positionColumn
580
};
581
});
582
583
// Sort delete operations
584
operations.sort((a, b) => {
585
if (a.startLineNumber === b.startLineNumber) {
586
return a.endLineNumber - b.endLineNumber;
587
}
588
return a.startLineNumber - b.startLineNumber;
589
});
590
591
// Merge delete operations which are adjacent or overlapping
592
const mergedOperations: IDeleteLinesOperation[] = [];
593
let previousOperation = operations[0];
594
for (let i = 1; i < operations.length; i++) {
595
if (previousOperation.endLineNumber + 1 >= operations[i].startLineNumber) {
596
// Merge current operations into the previous one
597
previousOperation.endLineNumber = operations[i].endLineNumber;
598
} else {
599
// Push previous operation
600
mergedOperations.push(previousOperation);
601
previousOperation = operations[i];
602
}
603
}
604
// Push the last operation
605
mergedOperations.push(previousOperation);
606
607
return mergedOperations;
608
}
609
}
610
611
export class IndentLinesAction extends EditorAction {
612
constructor() {
613
super({
614
id: 'editor.action.indentLines',
615
label: nls.localize2('lines.indent', "Indent Line"),
616
precondition: EditorContextKeys.writable,
617
kbOpts: {
618
kbExpr: EditorContextKeys.editorTextFocus,
619
primary: KeyMod.CtrlCmd | KeyCode.BracketRight,
620
weight: KeybindingWeight.EditorContrib
621
},
622
canTriggerInlineEdits: true,
623
});
624
}
625
626
public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {
627
const viewModel = editor._getViewModel();
628
if (!viewModel) {
629
return;
630
}
631
editor.pushUndoStop();
632
editor.executeCommands(this.id, TypeOperations.indent(viewModel.cursorConfig, editor.getModel(), editor.getSelections()));
633
editor.pushUndoStop();
634
}
635
}
636
637
class OutdentLinesAction extends EditorAction {
638
constructor() {
639
super({
640
id: 'editor.action.outdentLines',
641
label: nls.localize2('lines.outdent', "Outdent Line"),
642
precondition: EditorContextKeys.writable,
643
kbOpts: {
644
kbExpr: EditorContextKeys.editorTextFocus,
645
primary: KeyMod.CtrlCmd | KeyCode.BracketLeft,
646
weight: KeybindingWeight.EditorContrib
647
},
648
canTriggerInlineEdits: true,
649
});
650
}
651
652
public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {
653
CoreEditingCommands.Outdent.runEditorCommand(_accessor, editor, null);
654
}
655
}
656
657
export class InsertLineBeforeAction extends EditorAction {
658
public static readonly ID = 'editor.action.insertLineBefore';
659
constructor() {
660
super({
661
id: InsertLineBeforeAction.ID,
662
label: nls.localize2('lines.insertBefore', "Insert Line Above"),
663
precondition: EditorContextKeys.writable,
664
kbOpts: {
665
kbExpr: EditorContextKeys.editorTextFocus,
666
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter,
667
weight: KeybindingWeight.EditorContrib
668
},
669
canTriggerInlineEdits: true,
670
});
671
}
672
673
public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {
674
const viewModel = editor._getViewModel();
675
if (!viewModel) {
676
return;
677
}
678
editor.pushUndoStop();
679
editor.executeCommands(this.id, EnterOperation.lineInsertBefore(viewModel.cursorConfig, editor.getModel(), editor.getSelections()));
680
}
681
}
682
683
export class InsertLineAfterAction extends EditorAction {
684
public static readonly ID = 'editor.action.insertLineAfter';
685
constructor() {
686
super({
687
id: InsertLineAfterAction.ID,
688
label: nls.localize2('lines.insertAfter', "Insert Line Below"),
689
precondition: EditorContextKeys.writable,
690
kbOpts: {
691
kbExpr: EditorContextKeys.editorTextFocus,
692
primary: KeyMod.CtrlCmd | KeyCode.Enter,
693
weight: KeybindingWeight.EditorContrib
694
},
695
canTriggerInlineEdits: true,
696
});
697
}
698
699
public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {
700
const viewModel = editor._getViewModel();
701
if (!viewModel) {
702
return;
703
}
704
editor.pushUndoStop();
705
editor.executeCommands(this.id, EnterOperation.lineInsertAfter(viewModel.cursorConfig, editor.getModel(), editor.getSelections()));
706
}
707
}
708
709
export abstract class AbstractDeleteAllToBoundaryAction extends EditorAction {
710
public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {
711
if (!editor.hasModel()) {
712
return;
713
}
714
const primaryCursor = editor.getSelection();
715
716
const rangesToDelete = this._getRangesToDelete(editor);
717
// merge overlapping selections
718
const effectiveRanges: Range[] = [];
719
720
for (let i = 0, count = rangesToDelete.length - 1; i < count; i++) {
721
const range = rangesToDelete[i];
722
const nextRange = rangesToDelete[i + 1];
723
724
if (Range.intersectRanges(range, nextRange) === null) {
725
effectiveRanges.push(range);
726
} else {
727
rangesToDelete[i + 1] = Range.plusRange(range, nextRange);
728
}
729
}
730
731
effectiveRanges.push(rangesToDelete[rangesToDelete.length - 1]);
732
733
const endCursorState = this._getEndCursorState(primaryCursor, effectiveRanges);
734
735
const edits: ISingleEditOperation[] = effectiveRanges.map(range => {
736
return EditOperation.replace(range, '');
737
});
738
739
editor.pushUndoStop();
740
editor.executeEdits(this.id, edits, endCursorState);
741
editor.pushUndoStop();
742
}
743
744
/**
745
* Compute the cursor state after the edit operations were applied.
746
*/
747
protected abstract _getEndCursorState(primaryCursor: Range, rangesToDelete: Range[]): Selection[];
748
749
protected abstract _getRangesToDelete(editor: IActiveCodeEditor): Range[];
750
}
751
752
export class DeleteAllLeftAction extends AbstractDeleteAllToBoundaryAction {
753
constructor() {
754
super({
755
id: 'deleteAllLeft',
756
label: nls.localize2('lines.deleteAllLeft', "Delete All Left"),
757
precondition: EditorContextKeys.writable,
758
kbOpts: {
759
kbExpr: EditorContextKeys.textInputFocus,
760
primary: 0,
761
mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace },
762
weight: KeybindingWeight.EditorContrib
763
},
764
canTriggerInlineEdits: true,
765
});
766
}
767
768
protected _getEndCursorState(primaryCursor: Range, rangesToDelete: Range[]): Selection[] {
769
let endPrimaryCursor: Selection | null = null;
770
const endCursorState: Selection[] = [];
771
let deletedLines = 0;
772
773
rangesToDelete.forEach(range => {
774
let endCursor;
775
if (range.endColumn === 1 && deletedLines > 0) {
776
const newStartLine = range.startLineNumber - deletedLines;
777
endCursor = new Selection(newStartLine, range.startColumn, newStartLine, range.startColumn);
778
} else {
779
endCursor = new Selection(range.startLineNumber, range.startColumn, range.startLineNumber, range.startColumn);
780
}
781
782
deletedLines += range.endLineNumber - range.startLineNumber;
783
784
if (range.intersectRanges(primaryCursor)) {
785
endPrimaryCursor = endCursor;
786
} else {
787
endCursorState.push(endCursor);
788
}
789
});
790
791
if (endPrimaryCursor) {
792
endCursorState.unshift(endPrimaryCursor);
793
}
794
795
return endCursorState;
796
}
797
798
protected _getRangesToDelete(editor: IActiveCodeEditor): Range[] {
799
const selections = editor.getSelections();
800
if (selections === null) {
801
return [];
802
}
803
804
let rangesToDelete: Range[] = selections;
805
const model = editor.getModel();
806
807
if (model === null) {
808
return [];
809
}
810
811
rangesToDelete.sort(Range.compareRangesUsingStarts);
812
rangesToDelete = rangesToDelete.map(selection => {
813
if (selection.isEmpty()) {
814
if (selection.startColumn === 1) {
815
const deleteFromLine = Math.max(1, selection.startLineNumber - 1);
816
const deleteFromColumn = selection.startLineNumber === 1 ? 1 : model.getLineLength(deleteFromLine) + 1;
817
return new Range(deleteFromLine, deleteFromColumn, selection.startLineNumber, 1);
818
} else {
819
return new Range(selection.startLineNumber, 1, selection.startLineNumber, selection.startColumn);
820
}
821
} else {
822
return new Range(selection.startLineNumber, 1, selection.endLineNumber, selection.endColumn);
823
}
824
});
825
826
return rangesToDelete;
827
}
828
}
829
830
export class DeleteAllRightAction extends AbstractDeleteAllToBoundaryAction {
831
constructor() {
832
super({
833
id: 'deleteAllRight',
834
label: nls.localize2('lines.deleteAllRight', "Delete All Right"),
835
precondition: EditorContextKeys.writable,
836
kbOpts: {
837
kbExpr: EditorContextKeys.textInputFocus,
838
primary: 0,
839
mac: { primary: KeyMod.WinCtrl | KeyCode.KeyK, secondary: [KeyMod.CtrlCmd | KeyCode.Delete] },
840
weight: KeybindingWeight.EditorContrib
841
},
842
canTriggerInlineEdits: true,
843
});
844
}
845
846
protected _getEndCursorState(primaryCursor: Range, rangesToDelete: Range[]): Selection[] {
847
let endPrimaryCursor: Selection | null = null;
848
const endCursorState: Selection[] = [];
849
for (let i = 0, len = rangesToDelete.length, offset = 0; i < len; i++) {
850
const range = rangesToDelete[i];
851
const endCursor = new Selection(range.startLineNumber - offset, range.startColumn, range.startLineNumber - offset, range.startColumn);
852
853
if (range.intersectRanges(primaryCursor)) {
854
endPrimaryCursor = endCursor;
855
} else {
856
endCursorState.push(endCursor);
857
}
858
}
859
860
if (endPrimaryCursor) {
861
endCursorState.unshift(endPrimaryCursor);
862
}
863
864
return endCursorState;
865
}
866
867
protected _getRangesToDelete(editor: IActiveCodeEditor): Range[] {
868
const model = editor.getModel();
869
if (model === null) {
870
return [];
871
}
872
873
const selections = editor.getSelections();
874
875
if (selections === null) {
876
return [];
877
}
878
879
const rangesToDelete: Range[] = selections.map((sel) => {
880
if (sel.isEmpty()) {
881
const maxColumn = model.getLineMaxColumn(sel.startLineNumber);
882
883
if (sel.startColumn === maxColumn) {
884
return new Range(sel.startLineNumber, sel.startColumn, sel.startLineNumber + 1, 1);
885
} else {
886
return new Range(sel.startLineNumber, sel.startColumn, sel.startLineNumber, maxColumn);
887
}
888
}
889
return sel;
890
});
891
892
rangesToDelete.sort(Range.compareRangesUsingStarts);
893
return rangesToDelete;
894
}
895
}
896
897
export class JoinLinesAction extends EditorAction {
898
constructor() {
899
super({
900
id: 'editor.action.joinLines',
901
label: nls.localize2('lines.joinLines', "Join Lines"),
902
precondition: EditorContextKeys.writable,
903
kbOpts: {
904
kbExpr: EditorContextKeys.editorTextFocus,
905
primary: 0,
906
mac: { primary: KeyMod.WinCtrl | KeyCode.KeyJ },
907
weight: KeybindingWeight.EditorContrib
908
},
909
canTriggerInlineEdits: true,
910
});
911
}
912
913
public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {
914
const selections = editor.getSelections();
915
if (selections === null) {
916
return;
917
}
918
919
let primaryCursor = editor.getSelection();
920
if (primaryCursor === null) {
921
return;
922
}
923
924
selections.sort(Range.compareRangesUsingStarts);
925
const reducedSelections: Selection[] = [];
926
927
const lastSelection = selections.reduce((previousValue, currentValue) => {
928
if (previousValue.isEmpty()) {
929
if (previousValue.endLineNumber === currentValue.startLineNumber) {
930
if (primaryCursor!.equalsSelection(previousValue)) {
931
primaryCursor = currentValue;
932
}
933
return currentValue;
934
}
935
936
if (currentValue.startLineNumber > previousValue.endLineNumber + 1) {
937
reducedSelections.push(previousValue);
938
return currentValue;
939
} else {
940
return new Selection(previousValue.startLineNumber, previousValue.startColumn, currentValue.endLineNumber, currentValue.endColumn);
941
}
942
} else {
943
if (currentValue.startLineNumber > previousValue.endLineNumber) {
944
reducedSelections.push(previousValue);
945
return currentValue;
946
} else {
947
return new Selection(previousValue.startLineNumber, previousValue.startColumn, currentValue.endLineNumber, currentValue.endColumn);
948
}
949
}
950
});
951
952
reducedSelections.push(lastSelection);
953
954
const model = editor.getModel();
955
if (model === null) {
956
return;
957
}
958
959
const edits: ISingleEditOperation[] = [];
960
const endCursorState: Selection[] = [];
961
let endPrimaryCursor = primaryCursor;
962
let lineOffset = 0;
963
964
for (let i = 0, len = reducedSelections.length; i < len; i++) {
965
const selection = reducedSelections[i];
966
const startLineNumber = selection.startLineNumber;
967
const startColumn = 1;
968
let columnDeltaOffset = 0;
969
let endLineNumber: number,
970
endColumn: number;
971
972
const selectionEndPositionOffset = model.getLineLength(selection.endLineNumber) - selection.endColumn;
973
974
if (selection.isEmpty() || selection.startLineNumber === selection.endLineNumber) {
975
const position = selection.getStartPosition();
976
if (position.lineNumber < model.getLineCount()) {
977
endLineNumber = startLineNumber + 1;
978
endColumn = model.getLineMaxColumn(endLineNumber);
979
} else {
980
endLineNumber = position.lineNumber;
981
endColumn = model.getLineMaxColumn(position.lineNumber);
982
}
983
} else {
984
endLineNumber = selection.endLineNumber;
985
endColumn = model.getLineMaxColumn(endLineNumber);
986
}
987
988
let trimmedLinesContent = model.getLineContent(startLineNumber);
989
990
for (let i = startLineNumber + 1; i <= endLineNumber; i++) {
991
const lineText = model.getLineContent(i);
992
const firstNonWhitespaceIdx = model.getLineFirstNonWhitespaceColumn(i);
993
994
if (firstNonWhitespaceIdx >= 1) {
995
let insertSpace = true;
996
if (trimmedLinesContent === '') {
997
insertSpace = false;
998
}
999
1000
if (insertSpace && (trimmedLinesContent.charAt(trimmedLinesContent.length - 1) === ' ' ||
1001
trimmedLinesContent.charAt(trimmedLinesContent.length - 1) === '\t')) {
1002
insertSpace = false;
1003
trimmedLinesContent = trimmedLinesContent.replace(/[\s\uFEFF\xA0]+$/g, ' ');
1004
}
1005
1006
const lineTextWithoutIndent = lineText.substr(firstNonWhitespaceIdx - 1);
1007
1008
trimmedLinesContent += (insertSpace ? ' ' : '') + lineTextWithoutIndent;
1009
1010
if (insertSpace) {
1011
columnDeltaOffset = lineTextWithoutIndent.length + 1;
1012
} else {
1013
columnDeltaOffset = lineTextWithoutIndent.length;
1014
}
1015
} else {
1016
columnDeltaOffset = 0;
1017
}
1018
}
1019
1020
const deleteSelection = new Range(startLineNumber, startColumn, endLineNumber, endColumn);
1021
1022
if (!deleteSelection.isEmpty()) {
1023
let resultSelection: Selection;
1024
1025
if (selection.isEmpty()) {
1026
edits.push(EditOperation.replace(deleteSelection, trimmedLinesContent));
1027
resultSelection = new Selection(deleteSelection.startLineNumber - lineOffset, trimmedLinesContent.length - columnDeltaOffset + 1, startLineNumber - lineOffset, trimmedLinesContent.length - columnDeltaOffset + 1);
1028
} else {
1029
if (selection.startLineNumber === selection.endLineNumber) {
1030
edits.push(EditOperation.replace(deleteSelection, trimmedLinesContent));
1031
resultSelection = new Selection(selection.startLineNumber - lineOffset, selection.startColumn,
1032
selection.endLineNumber - lineOffset, selection.endColumn);
1033
} else {
1034
edits.push(EditOperation.replace(deleteSelection, trimmedLinesContent));
1035
resultSelection = new Selection(selection.startLineNumber - lineOffset, selection.startColumn,
1036
selection.startLineNumber - lineOffset, trimmedLinesContent.length - selectionEndPositionOffset);
1037
}
1038
}
1039
1040
if (Range.intersectRanges(deleteSelection, primaryCursor) !== null) {
1041
endPrimaryCursor = resultSelection;
1042
} else {
1043
endCursorState.push(resultSelection);
1044
}
1045
}
1046
1047
lineOffset += deleteSelection.endLineNumber - deleteSelection.startLineNumber;
1048
}
1049
1050
endCursorState.unshift(endPrimaryCursor);
1051
editor.pushUndoStop();
1052
editor.executeEdits(this.id, edits, endCursorState);
1053
editor.pushUndoStop();
1054
}
1055
}
1056
1057
export class TransposeAction extends EditorAction {
1058
constructor() {
1059
super({
1060
id: 'editor.action.transpose',
1061
label: nls.localize2('editor.transpose', "Transpose Characters around the Cursor"),
1062
precondition: EditorContextKeys.writable,
1063
canTriggerInlineEdits: true,
1064
});
1065
}
1066
1067
public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {
1068
const selections = editor.getSelections();
1069
if (selections === null) {
1070
return;
1071
}
1072
1073
const model = editor.getModel();
1074
if (model === null) {
1075
return;
1076
}
1077
1078
const commands: ICommand[] = [];
1079
1080
for (let i = 0, len = selections.length; i < len; i++) {
1081
const selection = selections[i];
1082
1083
if (!selection.isEmpty()) {
1084
continue;
1085
}
1086
1087
const cursor = selection.getStartPosition();
1088
const maxColumn = model.getLineMaxColumn(cursor.lineNumber);
1089
1090
if (cursor.column >= maxColumn) {
1091
if (cursor.lineNumber === model.getLineCount()) {
1092
continue;
1093
}
1094
1095
// The cursor is at the end of current line and current line is not empty
1096
// then we transpose the character before the cursor and the line break if there is any following line.
1097
const deleteSelection = new Range(cursor.lineNumber, Math.max(1, cursor.column - 1), cursor.lineNumber + 1, 1);
1098
const chars = model.getValueInRange(deleteSelection).split('').reverse().join('');
1099
1100
commands.push(new ReplaceCommand(new Selection(cursor.lineNumber, Math.max(1, cursor.column - 1), cursor.lineNumber + 1, 1), chars));
1101
} else {
1102
const deleteSelection = new Range(cursor.lineNumber, Math.max(1, cursor.column - 1), cursor.lineNumber, cursor.column + 1);
1103
const chars = model.getValueInRange(deleteSelection).split('').reverse().join('');
1104
commands.push(new ReplaceCommandThatPreservesSelection(deleteSelection, chars,
1105
new Selection(cursor.lineNumber, cursor.column + 1, cursor.lineNumber, cursor.column + 1)));
1106
}
1107
}
1108
1109
editor.pushUndoStop();
1110
editor.executeCommands(this.id, commands);
1111
editor.pushUndoStop();
1112
}
1113
}
1114
1115
export abstract class AbstractCaseAction extends EditorAction {
1116
public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {
1117
const selections = editor.getSelections();
1118
if (selections === null) {
1119
return;
1120
}
1121
1122
const model = editor.getModel();
1123
if (model === null) {
1124
return;
1125
}
1126
1127
const wordSeparators = editor.getOption(EditorOption.wordSeparators);
1128
const textEdits: ISingleEditOperation[] = [];
1129
1130
for (const selection of selections) {
1131
if (selection.isEmpty()) {
1132
const cursor = selection.getStartPosition();
1133
const word = editor.getConfiguredWordAtPosition(cursor);
1134
1135
if (!word) {
1136
continue;
1137
}
1138
1139
const wordRange = new Range(cursor.lineNumber, word.startColumn, cursor.lineNumber, word.endColumn);
1140
const text = model.getValueInRange(wordRange);
1141
textEdits.push(EditOperation.replace(wordRange, this._modifyText(text, wordSeparators)));
1142
} else {
1143
const text = model.getValueInRange(selection);
1144
textEdits.push(EditOperation.replace(selection, this._modifyText(text, wordSeparators)));
1145
}
1146
}
1147
1148
editor.pushUndoStop();
1149
editor.executeEdits(this.id, textEdits);
1150
editor.pushUndoStop();
1151
}
1152
1153
protected abstract _modifyText(text: string, wordSeparators: string): string;
1154
}
1155
1156
export class UpperCaseAction extends AbstractCaseAction {
1157
constructor() {
1158
super({
1159
id: 'editor.action.transformToUppercase',
1160
label: nls.localize2('editor.transformToUppercase', "Transform to Uppercase"),
1161
precondition: EditorContextKeys.writable,
1162
canTriggerInlineEdits: true,
1163
});
1164
}
1165
1166
protected _modifyText(text: string, wordSeparators: string): string {
1167
return text.toLocaleUpperCase();
1168
}
1169
}
1170
1171
export class LowerCaseAction extends AbstractCaseAction {
1172
constructor() {
1173
super({
1174
id: 'editor.action.transformToLowercase',
1175
label: nls.localize2('editor.transformToLowercase', "Transform to Lowercase"),
1176
precondition: EditorContextKeys.writable,
1177
canTriggerInlineEdits: true
1178
});
1179
}
1180
1181
protected _modifyText(text: string, wordSeparators: string): string {
1182
return text.toLocaleLowerCase();
1183
}
1184
}
1185
1186
class BackwardsCompatibleRegExp {
1187
1188
private _actual: RegExp | null;
1189
private _evaluated: boolean;
1190
1191
constructor(
1192
private readonly _pattern: string,
1193
private readonly _flags: string
1194
) {
1195
this._actual = null;
1196
this._evaluated = false;
1197
}
1198
1199
public get(): RegExp | null {
1200
if (!this._evaluated) {
1201
this._evaluated = true;
1202
try {
1203
this._actual = new RegExp(this._pattern, this._flags);
1204
} catch (err) {
1205
// this browser does not support this regular expression
1206
}
1207
}
1208
return this._actual;
1209
}
1210
1211
public isSupported(): boolean {
1212
return (this.get() !== null);
1213
}
1214
}
1215
1216
export class TitleCaseAction extends AbstractCaseAction {
1217
1218
public static titleBoundary = new BackwardsCompatibleRegExp('(^|[^\\p{L}\\p{N}\']|((^|\\P{L})\'))\\p{L}', 'gmu');
1219
1220
constructor() {
1221
super({
1222
id: 'editor.action.transformToTitlecase',
1223
label: nls.localize2('editor.transformToTitlecase', "Transform to Title Case"),
1224
precondition: EditorContextKeys.writable,
1225
canTriggerInlineEdits: true
1226
});
1227
}
1228
1229
protected _modifyText(text: string, wordSeparators: string): string {
1230
const titleBoundary = TitleCaseAction.titleBoundary.get();
1231
if (!titleBoundary) {
1232
// cannot support this
1233
return text;
1234
}
1235
return text
1236
.toLocaleLowerCase()
1237
.replace(titleBoundary, (b) => b.toLocaleUpperCase());
1238
}
1239
}
1240
1241
export class SnakeCaseAction extends AbstractCaseAction {
1242
1243
public static caseBoundary = new BackwardsCompatibleRegExp('(\\p{Ll})(\\p{Lu})', 'gmu');
1244
public static singleLetters = new BackwardsCompatibleRegExp('(\\p{Lu}|\\p{N})(\\p{Lu})(\\p{Ll})', 'gmu');
1245
1246
constructor() {
1247
super({
1248
id: 'editor.action.transformToSnakecase',
1249
label: nls.localize2('editor.transformToSnakecase', "Transform to Snake Case"),
1250
precondition: EditorContextKeys.writable,
1251
canTriggerInlineEdits: true,
1252
});
1253
}
1254
1255
protected _modifyText(text: string, wordSeparators: string): string {
1256
const caseBoundary = SnakeCaseAction.caseBoundary.get();
1257
const singleLetters = SnakeCaseAction.singleLetters.get();
1258
if (!caseBoundary || !singleLetters) {
1259
// cannot support this
1260
return text;
1261
}
1262
return (text
1263
.replace(caseBoundary, '$1_$2')
1264
.replace(singleLetters, '$1_$2$3')
1265
.toLocaleLowerCase()
1266
);
1267
}
1268
}
1269
1270
export class CamelCaseAction extends AbstractCaseAction {
1271
public static singleLineWordBoundary = new BackwardsCompatibleRegExp('[_\\s-]+', 'gm');
1272
public static multiLineWordBoundary = new BackwardsCompatibleRegExp('[_-]+', 'gm');
1273
public static validWordStart = new BackwardsCompatibleRegExp('^(\\p{Lu}[^\\p{Lu}])', 'gmu');
1274
1275
constructor() {
1276
super({
1277
id: 'editor.action.transformToCamelcase',
1278
label: nls.localize2('editor.transformToCamelcase', "Transform to Camel Case"),
1279
precondition: EditorContextKeys.writable,
1280
canTriggerInlineEdits: true
1281
});
1282
}
1283
1284
protected _modifyText(text: string, wordSeparators: string): string {
1285
const wordBoundary = /\r\n|\r|\n/.test(text) ? CamelCaseAction.multiLineWordBoundary.get() : CamelCaseAction.singleLineWordBoundary.get();
1286
const validWordStart = CamelCaseAction.validWordStart.get();
1287
if (!wordBoundary || !validWordStart) {
1288
// cannot support this
1289
return text;
1290
}
1291
const words = text.split(wordBoundary);
1292
const firstWord = words.shift()?.replace(validWordStart, (start: string) => start.toLocaleLowerCase());
1293
return firstWord + words.map((word: string) => word.substring(0, 1).toLocaleUpperCase() + word.substring(1))
1294
.join('');
1295
}
1296
}
1297
1298
export class PascalCaseAction extends AbstractCaseAction {
1299
public static wordBoundary = new BackwardsCompatibleRegExp('[_ \\t-]', 'gm');
1300
public static wordBoundaryToMaintain = new BackwardsCompatibleRegExp('(?<=\\.)', 'gm');
1301
public static upperCaseWordMatcher = new BackwardsCompatibleRegExp('^\\p{Lu}+$', 'mu');
1302
1303
constructor() {
1304
super({
1305
id: 'editor.action.transformToPascalcase',
1306
label: nls.localize2('editor.transformToPascalcase', "Transform to Pascal Case"),
1307
precondition: EditorContextKeys.writable,
1308
canTriggerInlineEdits: true,
1309
});
1310
}
1311
1312
protected _modifyText(text: string, wordSeparators: string): string {
1313
const wordBoundary = PascalCaseAction.wordBoundary.get();
1314
const wordBoundaryToMaintain = PascalCaseAction.wordBoundaryToMaintain.get();
1315
const upperCaseWordMatcher = PascalCaseAction.upperCaseWordMatcher.get();
1316
1317
if (!wordBoundary || !wordBoundaryToMaintain || !upperCaseWordMatcher) {
1318
// cannot support this
1319
return text;
1320
}
1321
1322
const wordsWithMaintainBoundaries = text.split(wordBoundaryToMaintain);
1323
const words = wordsWithMaintainBoundaries.map(word => word.split(wordBoundary)).flat();
1324
1325
return words.map(word => {
1326
const normalizedWord = word.charAt(0).toLocaleUpperCase() + word.slice(1);
1327
const isAllCaps = normalizedWord.length > 1 && upperCaseWordMatcher.test(normalizedWord);
1328
if (isAllCaps) {
1329
return normalizedWord.charAt(0) + normalizedWord.slice(1).toLocaleLowerCase();
1330
}
1331
return normalizedWord;
1332
}).join('');
1333
}
1334
}
1335
1336
export class KebabCaseAction extends AbstractCaseAction {
1337
1338
public static isSupported(): boolean {
1339
const areAllRegexpsSupported = [
1340
this.caseBoundary,
1341
this.singleLetters,
1342
this.underscoreBoundary,
1343
].every((regexp) => regexp.isSupported());
1344
1345
return areAllRegexpsSupported;
1346
}
1347
1348
private static caseBoundary = new BackwardsCompatibleRegExp('(\\p{Ll})(\\p{Lu})', 'gmu');
1349
private static singleLetters = new BackwardsCompatibleRegExp('(\\p{Lu}|\\p{N})(\\p{Lu}\\p{Ll})', 'gmu');
1350
private static underscoreBoundary = new BackwardsCompatibleRegExp('(\\S)(_)(\\S)', 'gm');
1351
1352
constructor() {
1353
super({
1354
id: 'editor.action.transformToKebabcase',
1355
label: nls.localize2('editor.transformToKebabcase', 'Transform to Kebab Case'),
1356
precondition: EditorContextKeys.writable,
1357
canTriggerInlineEdits: true,
1358
});
1359
}
1360
1361
protected _modifyText(text: string, _: string): string {
1362
const caseBoundary = KebabCaseAction.caseBoundary.get();
1363
const singleLetters = KebabCaseAction.singleLetters.get();
1364
const underscoreBoundary = KebabCaseAction.underscoreBoundary.get();
1365
1366
if (!caseBoundary || !singleLetters || !underscoreBoundary) {
1367
// one or more regexps aren't supported
1368
return text;
1369
}
1370
1371
return text
1372
.replace(underscoreBoundary, '$1-$3')
1373
.replace(caseBoundary, '$1-$2')
1374
.replace(singleLetters, '$1-$2')
1375
.toLocaleLowerCase();
1376
}
1377
}
1378
1379
registerEditorAction(CopyLinesUpAction);
1380
registerEditorAction(CopyLinesDownAction);
1381
registerEditorAction(DuplicateSelectionAction);
1382
registerEditorAction(MoveLinesUpAction);
1383
registerEditorAction(MoveLinesDownAction);
1384
registerEditorAction(SortLinesAscendingAction);
1385
registerEditorAction(SortLinesDescendingAction);
1386
registerEditorAction(DeleteDuplicateLinesAction);
1387
registerEditorAction(TrimTrailingWhitespaceAction);
1388
registerEditorAction(DeleteLinesAction);
1389
registerEditorAction(IndentLinesAction);
1390
registerEditorAction(OutdentLinesAction);
1391
registerEditorAction(InsertLineBeforeAction);
1392
registerEditorAction(InsertLineAfterAction);
1393
registerEditorAction(DeleteAllLeftAction);
1394
registerEditorAction(DeleteAllRightAction);
1395
registerEditorAction(JoinLinesAction);
1396
registerEditorAction(TransposeAction);
1397
registerEditorAction(UpperCaseAction);
1398
registerEditorAction(LowerCaseAction);
1399
registerEditorAction(ReverseLinesAction);
1400
1401
if (SnakeCaseAction.caseBoundary.isSupported() && SnakeCaseAction.singleLetters.isSupported()) {
1402
registerEditorAction(SnakeCaseAction);
1403
}
1404
if (CamelCaseAction.singleLineWordBoundary.isSupported() && CamelCaseAction.multiLineWordBoundary.isSupported()) {
1405
registerEditorAction(CamelCaseAction);
1406
}
1407
if (PascalCaseAction.wordBoundary.isSupported()) {
1408
registerEditorAction(PascalCaseAction);
1409
}
1410
if (TitleCaseAction.titleBoundary.isSupported()) {
1411
registerEditorAction(TitleCaseAction);
1412
}
1413
1414
if (KebabCaseAction.isSupported()) {
1415
registerEditorAction(KebabCaseAction);
1416
}
1417
1418