Path: blob/main/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { KeyChord, KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';6import { CoreEditingCommands } from '../../../browser/coreCommands.js';7import { IActiveCodeEditor, ICodeEditor } from '../../../browser/editorBrowser.js';8import { EditorAction, IActionOptions, registerEditorAction, ServicesAccessor } from '../../../browser/editorExtensions.js';9import { ReplaceCommand, ReplaceCommandThatPreservesSelection, ReplaceCommandThatSelectsText } from '../../../common/commands/replaceCommand.js';10import { TrimTrailingWhitespaceCommand } from '../../../common/commands/trimTrailingWhitespaceCommand.js';11import { EditorOption } from '../../../common/config/editorOptions.js';12import { TypeOperations } from '../../../common/cursor/cursorTypeOperations.js';13import { EnterOperation } from '../../../common/cursor/cursorTypeEditOperations.js';14import { EditOperation, ISingleEditOperation } from '../../../common/core/editOperation.js';15import { Position } from '../../../common/core/position.js';16import { Range } from '../../../common/core/range.js';17import { Selection } from '../../../common/core/selection.js';18import { ICommand } from '../../../common/editorCommon.js';19import { EditorContextKeys } from '../../../common/editorContextKeys.js';20import { ITextModel } from '../../../common/model.js';21import { CopyLinesCommand } from './copyLinesCommand.js';22import { MoveLinesCommand } from './moveLinesCommand.js';23import { SortLinesCommand } from './sortLinesCommand.js';24import * as nls from '../../../../nls.js';25import { MenuId } from '../../../../platform/actions/common/actions.js';26import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';27import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js';28import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';2930// copy lines3132abstract class AbstractCopyLinesAction extends EditorAction {3334private readonly down: boolean;3536constructor(down: boolean, opts: IActionOptions) {37super(opts);38this.down = down;39}4041public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {42if (!editor.hasModel()) {43return;44}4546const selections = editor.getSelections().map((selection, index) => ({ selection, index, ignore: false }));47selections.sort((a, b) => Range.compareRangesUsingStarts(a.selection, b.selection));4849// Remove selections that would result in copying the same line50let prev = selections[0];51for (let i = 1; i < selections.length; i++) {52const curr = selections[i];53if (prev.selection.endLineNumber === curr.selection.startLineNumber) {54// these two selections would copy the same line55if (prev.index < curr.index) {56// prev wins57curr.ignore = true;58} else {59// curr wins60prev.ignore = true;61prev = curr;62}63}64}6566const commands: ICommand[] = [];67for (const selection of selections) {68commands.push(new CopyLinesCommand(selection.selection, this.down, selection.ignore));69}7071editor.pushUndoStop();72editor.executeCommands(this.id, commands);73editor.pushUndoStop();74}75}7677class CopyLinesUpAction extends AbstractCopyLinesAction {78constructor() {79super(false, {80id: 'editor.action.copyLinesUpAction',81label: nls.localize2('lines.copyUp', "Copy Line Up"),82precondition: EditorContextKeys.writable,83kbOpts: {84kbExpr: EditorContextKeys.editorTextFocus,85primary: KeyMod.Alt | KeyMod.Shift | KeyCode.UpArrow,86linux: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.UpArrow },87weight: KeybindingWeight.EditorContrib88},89menuOpts: {90menuId: MenuId.MenubarSelectionMenu,91group: '2_line',92title: nls.localize({ key: 'miCopyLinesUp', comment: ['&& denotes a mnemonic'] }, "&&Copy Line Up"),93order: 194}95});96}97}9899class CopyLinesDownAction extends AbstractCopyLinesAction {100constructor() {101super(true, {102id: 'editor.action.copyLinesDownAction',103label: nls.localize2('lines.copyDown', "Copy Line Down"),104precondition: EditorContextKeys.writable,105kbOpts: {106kbExpr: EditorContextKeys.editorTextFocus,107primary: KeyMod.Alt | KeyMod.Shift | KeyCode.DownArrow,108linux: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.DownArrow },109weight: KeybindingWeight.EditorContrib110},111menuOpts: {112menuId: MenuId.MenubarSelectionMenu,113group: '2_line',114title: nls.localize({ key: 'miCopyLinesDown', comment: ['&& denotes a mnemonic'] }, "Co&&py Line Down"),115order: 2116}117});118}119}120121export class DuplicateSelectionAction extends EditorAction {122123constructor() {124super({125id: 'editor.action.duplicateSelection',126label: nls.localize2('duplicateSelection', "Duplicate Selection"),127precondition: EditorContextKeys.writable,128menuOpts: {129menuId: MenuId.MenubarSelectionMenu,130group: '2_line',131title: nls.localize({ key: 'miDuplicateSelection', comment: ['&& denotes a mnemonic'] }, "&&Duplicate Selection"),132order: 5133}134});135}136137public run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void {138if (!editor.hasModel()) {139return;140}141142const commands: ICommand[] = [];143const selections = editor.getSelections();144const model = editor.getModel();145146for (const selection of selections) {147if (selection.isEmpty()) {148commands.push(new CopyLinesCommand(selection, true));149} else {150const insertSelection = new Selection(selection.endLineNumber, selection.endColumn, selection.endLineNumber, selection.endColumn);151commands.push(new ReplaceCommandThatSelectsText(insertSelection, model.getValueInRange(selection)));152}153}154155editor.pushUndoStop();156editor.executeCommands(this.id, commands);157editor.pushUndoStop();158}159}160161// move lines162163abstract class AbstractMoveLinesAction extends EditorAction {164165private readonly down: boolean;166167constructor(down: boolean, opts: IActionOptions) {168super(opts);169this.down = down;170}171172public run(accessor: ServicesAccessor, editor: ICodeEditor): void {173const languageConfigurationService = accessor.get(ILanguageConfigurationService);174175const commands: ICommand[] = [];176const selections = editor.getSelections() || [];177const autoIndent = editor.getOption(EditorOption.autoIndent);178179for (const selection of selections) {180commands.push(new MoveLinesCommand(selection, this.down, autoIndent, languageConfigurationService));181}182183editor.pushUndoStop();184editor.executeCommands(this.id, commands);185editor.pushUndoStop();186}187}188189class MoveLinesUpAction extends AbstractMoveLinesAction {190constructor() {191super(false, {192id: 'editor.action.moveLinesUpAction',193label: nls.localize2('lines.moveUp', "Move Line Up"),194precondition: EditorContextKeys.writable,195kbOpts: {196kbExpr: EditorContextKeys.editorTextFocus,197primary: KeyMod.Alt | KeyCode.UpArrow,198linux: { primary: KeyMod.Alt | KeyCode.UpArrow },199weight: KeybindingWeight.EditorContrib200},201menuOpts: {202menuId: MenuId.MenubarSelectionMenu,203group: '2_line',204title: nls.localize({ key: 'miMoveLinesUp', comment: ['&& denotes a mnemonic'] }, "Mo&&ve Line Up"),205order: 3206}207});208}209}210211class MoveLinesDownAction extends AbstractMoveLinesAction {212constructor() {213super(true, {214id: 'editor.action.moveLinesDownAction',215label: nls.localize2('lines.moveDown', "Move Line Down"),216precondition: EditorContextKeys.writable,217kbOpts: {218kbExpr: EditorContextKeys.editorTextFocus,219primary: KeyMod.Alt | KeyCode.DownArrow,220linux: { primary: KeyMod.Alt | KeyCode.DownArrow },221weight: KeybindingWeight.EditorContrib222},223menuOpts: {224menuId: MenuId.MenubarSelectionMenu,225group: '2_line',226title: nls.localize({ key: 'miMoveLinesDown', comment: ['&& denotes a mnemonic'] }, "Move &&Line Down"),227order: 4228}229});230}231}232233export abstract class AbstractSortLinesAction extends EditorAction {234private readonly descending: boolean;235236constructor(descending: boolean, opts: IActionOptions) {237super(opts);238this.descending = descending;239}240241public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {242if (!editor.hasModel()) {243return;244}245246const model = editor.getModel();247let selections = editor.getSelections();248if (selections.length === 1 && selections[0].isEmpty()) {249// Apply to whole document.250selections = [new Selection(1, 1, model.getLineCount(), model.getLineMaxColumn(model.getLineCount()))];251}252253for (const selection of selections) {254if (!SortLinesCommand.canRun(editor.getModel(), selection, this.descending)) {255return;256}257}258259const commands: ICommand[] = [];260for (let i = 0, len = selections.length; i < len; i++) {261commands[i] = new SortLinesCommand(selections[i], this.descending);262}263264editor.pushUndoStop();265editor.executeCommands(this.id, commands);266editor.pushUndoStop();267}268}269270export class SortLinesAscendingAction extends AbstractSortLinesAction {271constructor() {272super(false, {273id: 'editor.action.sortLinesAscending',274label: nls.localize2('lines.sortAscending', "Sort Lines Ascending"),275precondition: EditorContextKeys.writable276});277}278}279280export class SortLinesDescendingAction extends AbstractSortLinesAction {281constructor() {282super(true, {283id: 'editor.action.sortLinesDescending',284label: nls.localize2('lines.sortDescending', "Sort Lines Descending"),285precondition: EditorContextKeys.writable286});287}288}289290export class DeleteDuplicateLinesAction extends EditorAction {291constructor() {292super({293id: 'editor.action.removeDuplicateLines',294label: nls.localize2('lines.deleteDuplicates', "Delete Duplicate Lines"),295precondition: EditorContextKeys.writable296});297}298299public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {300if (!editor.hasModel()) {301return;302}303304const model: ITextModel = editor.getModel();305if (model.getLineCount() === 1 && model.getLineMaxColumn(1) === 1) {306return;307}308309const edits: ISingleEditOperation[] = [];310const endCursorState: Selection[] = [];311312let linesDeleted = 0;313let updateSelection = true;314315let selections = editor.getSelections();316if (selections.length === 1 && selections[0].isEmpty()) {317// Apply to whole document.318selections = [new Selection(1, 1, model.getLineCount(), model.getLineMaxColumn(model.getLineCount()))];319updateSelection = false;320}321322for (const selection of selections) {323const uniqueLines = new Set();324const lines = [];325326for (let i = selection.startLineNumber; i <= selection.endLineNumber; i++) {327const line = model.getLineContent(i);328329if (uniqueLines.has(line)) {330continue;331}332333lines.push(line);334uniqueLines.add(line);335}336337338const selectionToReplace = new Selection(339selection.startLineNumber,3401,341selection.endLineNumber,342model.getLineMaxColumn(selection.endLineNumber)343);344345const adjustedSelectionStart = selection.startLineNumber - linesDeleted;346const finalSelection = new Selection(347adjustedSelectionStart,3481,349adjustedSelectionStart + lines.length - 1,350lines[lines.length - 1].length351);352353edits.push(EditOperation.replace(selectionToReplace, lines.join('\n')));354endCursorState.push(finalSelection);355356linesDeleted += (selection.endLineNumber - selection.startLineNumber + 1) - lines.length;357}358359editor.pushUndoStop();360editor.executeEdits(this.id, edits, updateSelection ? endCursorState : undefined);361editor.pushUndoStop();362}363}364365export class ReverseLinesAction extends EditorAction {366constructor() {367super({368id: 'editor.action.reverseLines',369label: nls.localize2('lines.reverseLines', "Reverse lines"),370precondition: EditorContextKeys.writable371});372}373374public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {375if (!editor.hasModel()) {376return;377}378379const model: ITextModel = editor.getModel();380const originalSelections = editor.getSelections();381let selections = originalSelections;382if (selections.length === 1 && selections[0].isEmpty()) {383// Apply to whole document.384selections = [new Selection(1, 1, model.getLineCount(), model.getLineMaxColumn(model.getLineCount()))];385}386387const edits: ISingleEditOperation[] = [];388const resultingSelections: Selection[] = [];389390for (let i = 0; i < selections.length; i++) {391const selection = selections[i];392const originalSelection = originalSelections[i];393let endLineNumber = selection.endLineNumber;394if (selection.startLineNumber < selection.endLineNumber && selection.endColumn === 1) {395endLineNumber--;396}397398let range: Range = new Range(selection.startLineNumber, 1, endLineNumber, model.getLineMaxColumn(endLineNumber));399400// Exclude last line if empty and we're at the end of the document401if (endLineNumber === model.getLineCount() && model.getLineContent(range.endLineNumber) === '') {402range = range.setEndPosition(range.endLineNumber - 1, model.getLineMaxColumn(range.endLineNumber - 1));403}404405const lines: string[] = [];406for (let i = range.endLineNumber; i >= range.startLineNumber; i--) {407lines.push(model.getLineContent(i));408}409const edit: ISingleEditOperation = EditOperation.replace(range, lines.join('\n'));410edits.push(edit);411412const updateLineNumber = function (lineNumber: number): number {413return lineNumber <= range.endLineNumber ? range.endLineNumber - lineNumber + range.startLineNumber : lineNumber;414};415const updateSelection = function (sel: Selection): Selection {416if (sel.isEmpty()) {417// keep just the cursor418return new Selection(updateLineNumber(sel.positionLineNumber), sel.positionColumn, updateLineNumber(sel.positionLineNumber), sel.positionColumn);419} else {420// keep selection - maintain direction by creating backward selection421const newSelectionStart = updateLineNumber(sel.selectionStartLineNumber);422const newPosition = updateLineNumber(sel.positionLineNumber);423const newSelectionStartColumn = sel.selectionStartColumn;424const newPositionColumn = sel.positionColumn;425426// Create selection: from (newSelectionStart, newSelectionStartColumn) to (newPosition, newPositionColumn)427// After reversal: from (3, 2) to (1, 3)428return new Selection(newSelectionStart, newSelectionStartColumn, newPosition, newPositionColumn);429}430};431resultingSelections.push(updateSelection(originalSelection));432}433434editor.pushUndoStop();435editor.executeEdits(this.id, edits, resultingSelections);436editor.pushUndoStop();437}438}439440export class TrimTrailingWhitespaceAction extends EditorAction {441442public static readonly ID = 'editor.action.trimTrailingWhitespace';443444constructor() {445super({446id: TrimTrailingWhitespaceAction.ID,447label: nls.localize2('lines.trimTrailingWhitespace', "Trim Trailing Whitespace"),448precondition: EditorContextKeys.writable,449kbOpts: {450kbExpr: EditorContextKeys.editorTextFocus,451primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyX),452weight: KeybindingWeight.EditorContrib453}454});455}456457public run(_accessor: ServicesAccessor, editor: ICodeEditor, args: any): void {458459let cursors: Position[] = [];460if (args.reason === 'auto-save') {461// See https://github.com/editorconfig/editorconfig-vscode/issues/47462// It is very convenient for the editor config extension to invoke this action.463// So, if we get a reason:'auto-save' passed in, let's preserve cursor positions.464cursors = (editor.getSelections() || []).map(s => new Position(s.positionLineNumber, s.positionColumn));465}466467const selection = editor.getSelection();468if (selection === null) {469return;470}471472const config = _accessor.get(IConfigurationService);473const model = editor.getModel();474const trimInRegexAndStrings = config.getValue<boolean>('files.trimTrailingWhitespaceInRegexAndStrings', { overrideIdentifier: model?.getLanguageId(), resource: model?.uri });475476const command = new TrimTrailingWhitespaceCommand(selection, cursors, trimInRegexAndStrings);477478editor.pushUndoStop();479editor.executeCommands(this.id, [command]);480editor.pushUndoStop();481}482}483484// delete lines485486interface IDeleteLinesOperation {487startLineNumber: number;488selectionStartColumn: number;489endLineNumber: number;490positionColumn: number;491}492493export class DeleteLinesAction extends EditorAction {494495constructor() {496super({497id: 'editor.action.deleteLines',498label: nls.localize2('lines.delete', "Delete Line"),499precondition: EditorContextKeys.writable,500kbOpts: {501kbExpr: EditorContextKeys.textInputFocus,502primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyK,503weight: KeybindingWeight.EditorContrib504}505});506}507508public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {509if (!editor.hasModel()) {510return;511}512513const ops = this._getLinesToRemove(editor);514515const model: ITextModel = editor.getModel();516if (model.getLineCount() === 1 && model.getLineMaxColumn(1) === 1) {517// Model is empty518return;519}520521let linesDeleted = 0;522const edits: ISingleEditOperation[] = [];523const cursorState: Selection[] = [];524for (let i = 0, len = ops.length; i < len; i++) {525const op = ops[i];526527let startLineNumber = op.startLineNumber;528let endLineNumber = op.endLineNumber;529530let startColumn = 1;531let endColumn = model.getLineMaxColumn(endLineNumber);532if (endLineNumber < model.getLineCount()) {533endLineNumber += 1;534endColumn = 1;535} else if (startLineNumber > 1) {536startLineNumber -= 1;537startColumn = model.getLineMaxColumn(startLineNumber);538}539540edits.push(EditOperation.replace(new Selection(startLineNumber, startColumn, endLineNumber, endColumn), ''));541cursorState.push(new Selection(startLineNumber - linesDeleted, op.positionColumn, startLineNumber - linesDeleted, op.positionColumn));542linesDeleted += (op.endLineNumber - op.startLineNumber + 1);543}544545editor.pushUndoStop();546editor.executeEdits(this.id, edits, cursorState);547editor.pushUndoStop();548}549550private _getLinesToRemove(editor: IActiveCodeEditor): IDeleteLinesOperation[] {551// Construct delete operations552const operations: IDeleteLinesOperation[] = editor.getSelections().map((s) => {553554let endLineNumber = s.endLineNumber;555if (s.startLineNumber < s.endLineNumber && s.endColumn === 1) {556endLineNumber -= 1;557}558559return {560startLineNumber: s.startLineNumber,561selectionStartColumn: s.selectionStartColumn,562endLineNumber: endLineNumber,563positionColumn: s.positionColumn564};565});566567// Sort delete operations568operations.sort((a, b) => {569if (a.startLineNumber === b.startLineNumber) {570return a.endLineNumber - b.endLineNumber;571}572return a.startLineNumber - b.startLineNumber;573});574575// Merge delete operations which are adjacent or overlapping576const mergedOperations: IDeleteLinesOperation[] = [];577let previousOperation = operations[0];578for (let i = 1; i < operations.length; i++) {579if (previousOperation.endLineNumber + 1 >= operations[i].startLineNumber) {580// Merge current operations into the previous one581previousOperation.endLineNumber = operations[i].endLineNumber;582} else {583// Push previous operation584mergedOperations.push(previousOperation);585previousOperation = operations[i];586}587}588// Push the last operation589mergedOperations.push(previousOperation);590591return mergedOperations;592}593}594595export class IndentLinesAction extends EditorAction {596constructor() {597super({598id: 'editor.action.indentLines',599label: nls.localize2('lines.indent', "Indent Line"),600precondition: EditorContextKeys.writable,601kbOpts: {602kbExpr: EditorContextKeys.editorTextFocus,603primary: KeyMod.CtrlCmd | KeyCode.BracketRight,604weight: KeybindingWeight.EditorContrib605}606});607}608609public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {610const viewModel = editor._getViewModel();611if (!viewModel) {612return;613}614editor.pushUndoStop();615editor.executeCommands(this.id, TypeOperations.indent(viewModel.cursorConfig, editor.getModel(), editor.getSelections()));616editor.pushUndoStop();617}618}619620class OutdentLinesAction extends EditorAction {621constructor() {622super({623id: 'editor.action.outdentLines',624label: nls.localize2('lines.outdent', "Outdent Line"),625precondition: EditorContextKeys.writable,626kbOpts: {627kbExpr: EditorContextKeys.editorTextFocus,628primary: KeyMod.CtrlCmd | KeyCode.BracketLeft,629weight: KeybindingWeight.EditorContrib630}631});632}633634public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {635CoreEditingCommands.Outdent.runEditorCommand(_accessor, editor, null);636}637}638639export class InsertLineBeforeAction extends EditorAction {640constructor() {641super({642id: 'editor.action.insertLineBefore',643label: nls.localize2('lines.insertBefore', "Insert Line Above"),644precondition: EditorContextKeys.writable,645kbOpts: {646kbExpr: EditorContextKeys.editorTextFocus,647primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter,648weight: KeybindingWeight.EditorContrib649}650});651}652653public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {654const viewModel = editor._getViewModel();655if (!viewModel) {656return;657}658editor.pushUndoStop();659editor.executeCommands(this.id, EnterOperation.lineInsertBefore(viewModel.cursorConfig, editor.getModel(), editor.getSelections()));660}661}662663export class InsertLineAfterAction extends EditorAction {664constructor() {665super({666id: 'editor.action.insertLineAfter',667label: nls.localize2('lines.insertAfter', "Insert Line Below"),668precondition: EditorContextKeys.writable,669kbOpts: {670kbExpr: EditorContextKeys.editorTextFocus,671primary: KeyMod.CtrlCmd | KeyCode.Enter,672weight: KeybindingWeight.EditorContrib673}674});675}676677public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {678const viewModel = editor._getViewModel();679if (!viewModel) {680return;681}682editor.pushUndoStop();683editor.executeCommands(this.id, EnterOperation.lineInsertAfter(viewModel.cursorConfig, editor.getModel(), editor.getSelections()));684}685}686687export abstract class AbstractDeleteAllToBoundaryAction extends EditorAction {688public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {689if (!editor.hasModel()) {690return;691}692const primaryCursor = editor.getSelection();693694const rangesToDelete = this._getRangesToDelete(editor);695// merge overlapping selections696const effectiveRanges: Range[] = [];697698for (let i = 0, count = rangesToDelete.length - 1; i < count; i++) {699const range = rangesToDelete[i];700const nextRange = rangesToDelete[i + 1];701702if (Range.intersectRanges(range, nextRange) === null) {703effectiveRanges.push(range);704} else {705rangesToDelete[i + 1] = Range.plusRange(range, nextRange);706}707}708709effectiveRanges.push(rangesToDelete[rangesToDelete.length - 1]);710711const endCursorState = this._getEndCursorState(primaryCursor, effectiveRanges);712713const edits: ISingleEditOperation[] = effectiveRanges.map(range => {714return EditOperation.replace(range, '');715});716717editor.pushUndoStop();718editor.executeEdits(this.id, edits, endCursorState);719editor.pushUndoStop();720}721722/**723* Compute the cursor state after the edit operations were applied.724*/725protected abstract _getEndCursorState(primaryCursor: Range, rangesToDelete: Range[]): Selection[];726727protected abstract _getRangesToDelete(editor: IActiveCodeEditor): Range[];728}729730export class DeleteAllLeftAction extends AbstractDeleteAllToBoundaryAction {731constructor() {732super({733id: 'deleteAllLeft',734label: nls.localize2('lines.deleteAllLeft', "Delete All Left"),735precondition: EditorContextKeys.writable,736kbOpts: {737kbExpr: EditorContextKeys.textInputFocus,738primary: 0,739mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace },740weight: KeybindingWeight.EditorContrib741}742});743}744745protected _getEndCursorState(primaryCursor: Range, rangesToDelete: Range[]): Selection[] {746let endPrimaryCursor: Selection | null = null;747const endCursorState: Selection[] = [];748let deletedLines = 0;749750rangesToDelete.forEach(range => {751let endCursor;752if (range.endColumn === 1 && deletedLines > 0) {753const newStartLine = range.startLineNumber - deletedLines;754endCursor = new Selection(newStartLine, range.startColumn, newStartLine, range.startColumn);755} else {756endCursor = new Selection(range.startLineNumber, range.startColumn, range.startLineNumber, range.startColumn);757}758759deletedLines += range.endLineNumber - range.startLineNumber;760761if (range.intersectRanges(primaryCursor)) {762endPrimaryCursor = endCursor;763} else {764endCursorState.push(endCursor);765}766});767768if (endPrimaryCursor) {769endCursorState.unshift(endPrimaryCursor);770}771772return endCursorState;773}774775protected _getRangesToDelete(editor: IActiveCodeEditor): Range[] {776const selections = editor.getSelections();777if (selections === null) {778return [];779}780781let rangesToDelete: Range[] = selections;782const model = editor.getModel();783784if (model === null) {785return [];786}787788rangesToDelete.sort(Range.compareRangesUsingStarts);789rangesToDelete = rangesToDelete.map(selection => {790if (selection.isEmpty()) {791if (selection.startColumn === 1) {792const deleteFromLine = Math.max(1, selection.startLineNumber - 1);793const deleteFromColumn = selection.startLineNumber === 1 ? 1 : model.getLineLength(deleteFromLine) + 1;794return new Range(deleteFromLine, deleteFromColumn, selection.startLineNumber, 1);795} else {796return new Range(selection.startLineNumber, 1, selection.startLineNumber, selection.startColumn);797}798} else {799return new Range(selection.startLineNumber, 1, selection.endLineNumber, selection.endColumn);800}801});802803return rangesToDelete;804}805}806807export class DeleteAllRightAction extends AbstractDeleteAllToBoundaryAction {808constructor() {809super({810id: 'deleteAllRight',811label: nls.localize2('lines.deleteAllRight', "Delete All Right"),812precondition: EditorContextKeys.writable,813kbOpts: {814kbExpr: EditorContextKeys.textInputFocus,815primary: 0,816mac: { primary: KeyMod.WinCtrl | KeyCode.KeyK, secondary: [KeyMod.CtrlCmd | KeyCode.Delete] },817weight: KeybindingWeight.EditorContrib818}819});820}821822protected _getEndCursorState(primaryCursor: Range, rangesToDelete: Range[]): Selection[] {823let endPrimaryCursor: Selection | null = null;824const endCursorState: Selection[] = [];825for (let i = 0, len = rangesToDelete.length, offset = 0; i < len; i++) {826const range = rangesToDelete[i];827const endCursor = new Selection(range.startLineNumber - offset, range.startColumn, range.startLineNumber - offset, range.startColumn);828829if (range.intersectRanges(primaryCursor)) {830endPrimaryCursor = endCursor;831} else {832endCursorState.push(endCursor);833}834}835836if (endPrimaryCursor) {837endCursorState.unshift(endPrimaryCursor);838}839840return endCursorState;841}842843protected _getRangesToDelete(editor: IActiveCodeEditor): Range[] {844const model = editor.getModel();845if (model === null) {846return [];847}848849const selections = editor.getSelections();850851if (selections === null) {852return [];853}854855const rangesToDelete: Range[] = selections.map((sel) => {856if (sel.isEmpty()) {857const maxColumn = model.getLineMaxColumn(sel.startLineNumber);858859if (sel.startColumn === maxColumn) {860return new Range(sel.startLineNumber, sel.startColumn, sel.startLineNumber + 1, 1);861} else {862return new Range(sel.startLineNumber, sel.startColumn, sel.startLineNumber, maxColumn);863}864}865return sel;866});867868rangesToDelete.sort(Range.compareRangesUsingStarts);869return rangesToDelete;870}871}872873export class JoinLinesAction extends EditorAction {874constructor() {875super({876id: 'editor.action.joinLines',877label: nls.localize2('lines.joinLines', "Join Lines"),878precondition: EditorContextKeys.writable,879kbOpts: {880kbExpr: EditorContextKeys.editorTextFocus,881primary: 0,882mac: { primary: KeyMod.WinCtrl | KeyCode.KeyJ },883weight: KeybindingWeight.EditorContrib884}885});886}887888public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {889const selections = editor.getSelections();890if (selections === null) {891return;892}893894let primaryCursor = editor.getSelection();895if (primaryCursor === null) {896return;897}898899selections.sort(Range.compareRangesUsingStarts);900const reducedSelections: Selection[] = [];901902const lastSelection = selections.reduce((previousValue, currentValue) => {903if (previousValue.isEmpty()) {904if (previousValue.endLineNumber === currentValue.startLineNumber) {905if (primaryCursor!.equalsSelection(previousValue)) {906primaryCursor = currentValue;907}908return currentValue;909}910911if (currentValue.startLineNumber > previousValue.endLineNumber + 1) {912reducedSelections.push(previousValue);913return currentValue;914} else {915return new Selection(previousValue.startLineNumber, previousValue.startColumn, currentValue.endLineNumber, currentValue.endColumn);916}917} else {918if (currentValue.startLineNumber > previousValue.endLineNumber) {919reducedSelections.push(previousValue);920return currentValue;921} else {922return new Selection(previousValue.startLineNumber, previousValue.startColumn, currentValue.endLineNumber, currentValue.endColumn);923}924}925});926927reducedSelections.push(lastSelection);928929const model = editor.getModel();930if (model === null) {931return;932}933934const edits: ISingleEditOperation[] = [];935const endCursorState: Selection[] = [];936let endPrimaryCursor = primaryCursor;937let lineOffset = 0;938939for (let i = 0, len = reducedSelections.length; i < len; i++) {940const selection = reducedSelections[i];941const startLineNumber = selection.startLineNumber;942const startColumn = 1;943let columnDeltaOffset = 0;944let endLineNumber: number,945endColumn: number;946947const selectionEndPositionOffset = model.getLineLength(selection.endLineNumber) - selection.endColumn;948949if (selection.isEmpty() || selection.startLineNumber === selection.endLineNumber) {950const position = selection.getStartPosition();951if (position.lineNumber < model.getLineCount()) {952endLineNumber = startLineNumber + 1;953endColumn = model.getLineMaxColumn(endLineNumber);954} else {955endLineNumber = position.lineNumber;956endColumn = model.getLineMaxColumn(position.lineNumber);957}958} else {959endLineNumber = selection.endLineNumber;960endColumn = model.getLineMaxColumn(endLineNumber);961}962963let trimmedLinesContent = model.getLineContent(startLineNumber);964965for (let i = startLineNumber + 1; i <= endLineNumber; i++) {966const lineText = model.getLineContent(i);967const firstNonWhitespaceIdx = model.getLineFirstNonWhitespaceColumn(i);968969if (firstNonWhitespaceIdx >= 1) {970let insertSpace = true;971if (trimmedLinesContent === '') {972insertSpace = false;973}974975if (insertSpace && (trimmedLinesContent.charAt(trimmedLinesContent.length - 1) === ' ' ||976trimmedLinesContent.charAt(trimmedLinesContent.length - 1) === '\t')) {977insertSpace = false;978trimmedLinesContent = trimmedLinesContent.replace(/[\s\uFEFF\xA0]+$/g, ' ');979}980981const lineTextWithoutIndent = lineText.substr(firstNonWhitespaceIdx - 1);982983trimmedLinesContent += (insertSpace ? ' ' : '') + lineTextWithoutIndent;984985if (insertSpace) {986columnDeltaOffset = lineTextWithoutIndent.length + 1;987} else {988columnDeltaOffset = lineTextWithoutIndent.length;989}990} else {991columnDeltaOffset = 0;992}993}994995const deleteSelection = new Range(startLineNumber, startColumn, endLineNumber, endColumn);996997if (!deleteSelection.isEmpty()) {998let resultSelection: Selection;9991000if (selection.isEmpty()) {1001edits.push(EditOperation.replace(deleteSelection, trimmedLinesContent));1002resultSelection = new Selection(deleteSelection.startLineNumber - lineOffset, trimmedLinesContent.length - columnDeltaOffset + 1, startLineNumber - lineOffset, trimmedLinesContent.length - columnDeltaOffset + 1);1003} else {1004if (selection.startLineNumber === selection.endLineNumber) {1005edits.push(EditOperation.replace(deleteSelection, trimmedLinesContent));1006resultSelection = new Selection(selection.startLineNumber - lineOffset, selection.startColumn,1007selection.endLineNumber - lineOffset, selection.endColumn);1008} else {1009edits.push(EditOperation.replace(deleteSelection, trimmedLinesContent));1010resultSelection = new Selection(selection.startLineNumber - lineOffset, selection.startColumn,1011selection.startLineNumber - lineOffset, trimmedLinesContent.length - selectionEndPositionOffset);1012}1013}10141015if (Range.intersectRanges(deleteSelection, primaryCursor) !== null) {1016endPrimaryCursor = resultSelection;1017} else {1018endCursorState.push(resultSelection);1019}1020}10211022lineOffset += deleteSelection.endLineNumber - deleteSelection.startLineNumber;1023}10241025endCursorState.unshift(endPrimaryCursor);1026editor.pushUndoStop();1027editor.executeEdits(this.id, edits, endCursorState);1028editor.pushUndoStop();1029}1030}10311032export class TransposeAction extends EditorAction {1033constructor() {1034super({1035id: 'editor.action.transpose',1036label: nls.localize2('editor.transpose', "Transpose Characters around the Cursor"),1037precondition: EditorContextKeys.writable1038});1039}10401041public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {1042const selections = editor.getSelections();1043if (selections === null) {1044return;1045}10461047const model = editor.getModel();1048if (model === null) {1049return;1050}10511052const commands: ICommand[] = [];10531054for (let i = 0, len = selections.length; i < len; i++) {1055const selection = selections[i];10561057if (!selection.isEmpty()) {1058continue;1059}10601061const cursor = selection.getStartPosition();1062const maxColumn = model.getLineMaxColumn(cursor.lineNumber);10631064if (cursor.column >= maxColumn) {1065if (cursor.lineNumber === model.getLineCount()) {1066continue;1067}10681069// The cursor is at the end of current line and current line is not empty1070// then we transpose the character before the cursor and the line break if there is any following line.1071const deleteSelection = new Range(cursor.lineNumber, Math.max(1, cursor.column - 1), cursor.lineNumber + 1, 1);1072const chars = model.getValueInRange(deleteSelection).split('').reverse().join('');10731074commands.push(new ReplaceCommand(new Selection(cursor.lineNumber, Math.max(1, cursor.column - 1), cursor.lineNumber + 1, 1), chars));1075} else {1076const deleteSelection = new Range(cursor.lineNumber, Math.max(1, cursor.column - 1), cursor.lineNumber, cursor.column + 1);1077const chars = model.getValueInRange(deleteSelection).split('').reverse().join('');1078commands.push(new ReplaceCommandThatPreservesSelection(deleteSelection, chars,1079new Selection(cursor.lineNumber, cursor.column + 1, cursor.lineNumber, cursor.column + 1)));1080}1081}10821083editor.pushUndoStop();1084editor.executeCommands(this.id, commands);1085editor.pushUndoStop();1086}1087}10881089export abstract class AbstractCaseAction extends EditorAction {1090public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {1091const selections = editor.getSelections();1092if (selections === null) {1093return;1094}10951096const model = editor.getModel();1097if (model === null) {1098return;1099}11001101const wordSeparators = editor.getOption(EditorOption.wordSeparators);1102const textEdits: ISingleEditOperation[] = [];11031104for (const selection of selections) {1105if (selection.isEmpty()) {1106const cursor = selection.getStartPosition();1107const word = editor.getConfiguredWordAtPosition(cursor);11081109if (!word) {1110continue;1111}11121113const wordRange = new Range(cursor.lineNumber, word.startColumn, cursor.lineNumber, word.endColumn);1114const text = model.getValueInRange(wordRange);1115textEdits.push(EditOperation.replace(wordRange, this._modifyText(text, wordSeparators)));1116} else {1117const text = model.getValueInRange(selection);1118textEdits.push(EditOperation.replace(selection, this._modifyText(text, wordSeparators)));1119}1120}11211122editor.pushUndoStop();1123editor.executeEdits(this.id, textEdits);1124editor.pushUndoStop();1125}11261127protected abstract _modifyText(text: string, wordSeparators: string): string;1128}11291130export class UpperCaseAction extends AbstractCaseAction {1131constructor() {1132super({1133id: 'editor.action.transformToUppercase',1134label: nls.localize2('editor.transformToUppercase', "Transform to Uppercase"),1135precondition: EditorContextKeys.writable1136});1137}11381139protected _modifyText(text: string, wordSeparators: string): string {1140return text.toLocaleUpperCase();1141}1142}11431144export class LowerCaseAction extends AbstractCaseAction {1145constructor() {1146super({1147id: 'editor.action.transformToLowercase',1148label: nls.localize2('editor.transformToLowercase', "Transform to Lowercase"),1149precondition: EditorContextKeys.writable1150});1151}11521153protected _modifyText(text: string, wordSeparators: string): string {1154return text.toLocaleLowerCase();1155}1156}11571158class BackwardsCompatibleRegExp {11591160private _actual: RegExp | null;1161private _evaluated: boolean;11621163constructor(1164private readonly _pattern: string,1165private readonly _flags: string1166) {1167this._actual = null;1168this._evaluated = false;1169}11701171public get(): RegExp | null {1172if (!this._evaluated) {1173this._evaluated = true;1174try {1175this._actual = new RegExp(this._pattern, this._flags);1176} catch (err) {1177// this browser does not support this regular expression1178}1179}1180return this._actual;1181}11821183public isSupported(): boolean {1184return (this.get() !== null);1185}1186}11871188export class TitleCaseAction extends AbstractCaseAction {11891190public static titleBoundary = new BackwardsCompatibleRegExp('(^|[^\\p{L}\\p{N}\']|((^|\\P{L})\'))\\p{L}', 'gmu');11911192constructor() {1193super({1194id: 'editor.action.transformToTitlecase',1195label: nls.localize2('editor.transformToTitlecase', "Transform to Title Case"),1196precondition: EditorContextKeys.writable1197});1198}11991200protected _modifyText(text: string, wordSeparators: string): string {1201const titleBoundary = TitleCaseAction.titleBoundary.get();1202if (!titleBoundary) {1203// cannot support this1204return text;1205}1206return text1207.toLocaleLowerCase()1208.replace(titleBoundary, (b) => b.toLocaleUpperCase());1209}1210}12111212export class SnakeCaseAction extends AbstractCaseAction {12131214public static caseBoundary = new BackwardsCompatibleRegExp('(\\p{Ll})(\\p{Lu})', 'gmu');1215public static singleLetters = new BackwardsCompatibleRegExp('(\\p{Lu}|\\p{N})(\\p{Lu})(\\p{Ll})', 'gmu');12161217constructor() {1218super({1219id: 'editor.action.transformToSnakecase',1220label: nls.localize2('editor.transformToSnakecase', "Transform to Snake Case"),1221precondition: EditorContextKeys.writable1222});1223}12241225protected _modifyText(text: string, wordSeparators: string): string {1226const caseBoundary = SnakeCaseAction.caseBoundary.get();1227const singleLetters = SnakeCaseAction.singleLetters.get();1228if (!caseBoundary || !singleLetters) {1229// cannot support this1230return text;1231}1232return (text1233.replace(caseBoundary, '$1_$2')1234.replace(singleLetters, '$1_$2$3')1235.toLocaleLowerCase()1236);1237}1238}12391240export class CamelCaseAction extends AbstractCaseAction {1241public static wordBoundary = new BackwardsCompatibleRegExp('[_\\s-]', 'gm');1242public static validWordStart = new BackwardsCompatibleRegExp('^(\\p{Lu}[^\\p{Lu}])', 'gmu');12431244constructor() {1245super({1246id: 'editor.action.transformToCamelcase',1247label: nls.localize2('editor.transformToCamelcase', "Transform to Camel Case"),1248precondition: EditorContextKeys.writable1249});1250}12511252protected _modifyText(text: string, wordSeparators: string): string {1253const wordBoundary = CamelCaseAction.wordBoundary.get();1254const validWordStart = CamelCaseAction.validWordStart.get();1255if (!wordBoundary || !validWordStart) {1256// cannot support this1257return text;1258}1259const words = text.split(wordBoundary);1260const firstWord = words.shift()?.replace(validWordStart, (start: string) => start.toLocaleLowerCase());1261return firstWord + words.map((word: string) => word.substring(0, 1).toLocaleUpperCase() + word.substring(1))1262.join('');1263}1264}12651266export class PascalCaseAction extends AbstractCaseAction {1267public static wordBoundary = new BackwardsCompatibleRegExp('[_\\s-]', 'gm');1268public static wordBoundaryToMaintain = new BackwardsCompatibleRegExp('(?<=\\.)', 'gm');12691270constructor() {1271super({1272id: 'editor.action.transformToPascalcase',1273label: nls.localize2('editor.transformToPascalcase', "Transform to Pascal Case"),1274precondition: EditorContextKeys.writable1275});1276}12771278protected _modifyText(text: string, wordSeparators: string): string {1279const wordBoundary = PascalCaseAction.wordBoundary.get();1280const wordBoundaryToMaintain = PascalCaseAction.wordBoundaryToMaintain.get();12811282if (!wordBoundary || !wordBoundaryToMaintain) {1283// cannot support this1284return text;1285}12861287const wordsWithMaintainBoundaries = text.split(wordBoundaryToMaintain);1288const words = wordsWithMaintainBoundaries.map((word: string) => word.split(wordBoundary)).flat();1289return words.map((word: string) => word.substring(0, 1).toLocaleUpperCase() + word.substring(1))1290.join('');1291}1292}12931294export class KebabCaseAction extends AbstractCaseAction {12951296public static isSupported(): boolean {1297const areAllRegexpsSupported = [1298this.caseBoundary,1299this.singleLetters,1300this.underscoreBoundary,1301].every((regexp) => regexp.isSupported());13021303return areAllRegexpsSupported;1304}13051306private static caseBoundary = new BackwardsCompatibleRegExp('(\\p{Ll})(\\p{Lu})', 'gmu');1307private static singleLetters = new BackwardsCompatibleRegExp('(\\p{Lu}|\\p{N})(\\p{Lu}\\p{Ll})', 'gmu');1308private static underscoreBoundary = new BackwardsCompatibleRegExp('(\\S)(_)(\\S)', 'gm');13091310constructor() {1311super({1312id: 'editor.action.transformToKebabcase',1313label: nls.localize2('editor.transformToKebabcase', 'Transform to Kebab Case'),1314precondition: EditorContextKeys.writable1315});1316}13171318protected _modifyText(text: string, _: string): string {1319const caseBoundary = KebabCaseAction.caseBoundary.get();1320const singleLetters = KebabCaseAction.singleLetters.get();1321const underscoreBoundary = KebabCaseAction.underscoreBoundary.get();13221323if (!caseBoundary || !singleLetters || !underscoreBoundary) {1324// one or more regexps aren't supported1325return text;1326}13271328return text1329.replace(underscoreBoundary, '$1-$3')1330.replace(caseBoundary, '$1-$2')1331.replace(singleLetters, '$1-$2')1332.toLocaleLowerCase();1333}1334}13351336registerEditorAction(CopyLinesUpAction);1337registerEditorAction(CopyLinesDownAction);1338registerEditorAction(DuplicateSelectionAction);1339registerEditorAction(MoveLinesUpAction);1340registerEditorAction(MoveLinesDownAction);1341registerEditorAction(SortLinesAscendingAction);1342registerEditorAction(SortLinesDescendingAction);1343registerEditorAction(DeleteDuplicateLinesAction);1344registerEditorAction(TrimTrailingWhitespaceAction);1345registerEditorAction(DeleteLinesAction);1346registerEditorAction(IndentLinesAction);1347registerEditorAction(OutdentLinesAction);1348registerEditorAction(InsertLineBeforeAction);1349registerEditorAction(InsertLineAfterAction);1350registerEditorAction(DeleteAllLeftAction);1351registerEditorAction(DeleteAllRightAction);1352registerEditorAction(JoinLinesAction);1353registerEditorAction(TransposeAction);1354registerEditorAction(UpperCaseAction);1355registerEditorAction(LowerCaseAction);1356registerEditorAction(ReverseLinesAction);13571358if (SnakeCaseAction.caseBoundary.isSupported() && SnakeCaseAction.singleLetters.isSupported()) {1359registerEditorAction(SnakeCaseAction);1360}1361if (CamelCaseAction.wordBoundary.isSupported()) {1362registerEditorAction(CamelCaseAction);1363}1364if (PascalCaseAction.wordBoundary.isSupported()) {1365registerEditorAction(PascalCaseAction);1366}1367if (TitleCaseAction.titleBoundary.isSupported()) {1368registerEditorAction(TitleCaseAction);1369}13701371if (KebabCaseAction.isSupported()) {1372registerEditorAction(KebabCaseAction);1373}137413751376