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