Path: blob/main/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts
5220 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 * as nls from '../../../../nls.js';7import { MenuId } from '../../../../platform/actions/common/actions.js';8import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';9import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';10import { CoreEditingCommands } from '../../../browser/coreCommands.js';11import { IActiveCodeEditor, ICodeEditor } from '../../../browser/editorBrowser.js';12import { EditorAction, IActionOptions, registerEditorAction, ServicesAccessor } from '../../../browser/editorExtensions.js';13import { ReplaceCommand, ReplaceCommandThatPreservesSelection, ReplaceCommandThatSelectsText } from '../../../common/commands/replaceCommand.js';14import { TrimTrailingWhitespaceCommand } from '../../../common/commands/trimTrailingWhitespaceCommand.js';15import { EditorOption } from '../../../common/config/editorOptions.js';16import { EditOperation, ISingleEditOperation } from '../../../common/core/editOperation.js';17import { Position } from '../../../common/core/position.js';18import { Range } from '../../../common/core/range.js';19import { Selection } from '../../../common/core/selection.js';20import { EnterOperation } from '../../../common/cursor/cursorTypeEditOperations.js';21import { TypeOperations } from '../../../common/cursor/cursorTypeOperations.js';22import { ICommand } from '../../../common/editorCommon.js';23import { EditorContextKeys } from '../../../common/editorContextKeys.js';24import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js';25import { ITextModel } from '../../../common/model.js';26import { CopyLinesCommand } from './copyLinesCommand.js';27import { MoveLinesCommand } from './moveLinesCommand.js';28import { SortLinesCommand } from './sortLinesCommand.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},95canTriggerInlineEdits: true,96});97}98}99100class CopyLinesDownAction extends AbstractCopyLinesAction {101constructor() {102super(true, {103id: 'editor.action.copyLinesDownAction',104label: nls.localize2('lines.copyDown', "Copy Line Down"),105precondition: EditorContextKeys.writable,106kbOpts: {107kbExpr: EditorContextKeys.editorTextFocus,108primary: KeyMod.Alt | KeyMod.Shift | KeyCode.DownArrow,109linux: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.DownArrow },110weight: KeybindingWeight.EditorContrib111},112menuOpts: {113menuId: MenuId.MenubarSelectionMenu,114group: '2_line',115title: nls.localize({ key: 'miCopyLinesDown', comment: ['&& denotes a mnemonic'] }, "Co&&py Line Down"),116order: 2117},118canTriggerInlineEdits: true,119});120}121}122123export class DuplicateSelectionAction extends EditorAction {124125constructor() {126super({127id: 'editor.action.duplicateSelection',128label: nls.localize2('duplicateSelection', "Duplicate Selection"),129precondition: EditorContextKeys.writable,130menuOpts: {131menuId: MenuId.MenubarSelectionMenu,132group: '2_line',133title: nls.localize({ key: 'miDuplicateSelection', comment: ['&& denotes a mnemonic'] }, "&&Duplicate Selection"),134order: 5135},136canTriggerInlineEdits: true,137});138}139140public run(accessor: ServicesAccessor, editor: ICodeEditor, args: unknown): void {141if (!editor.hasModel()) {142return;143}144145const commands: ICommand[] = [];146const selections = editor.getSelections();147const model = editor.getModel();148149for (const selection of selections) {150if (selection.isEmpty()) {151commands.push(new CopyLinesCommand(selection, true));152} else {153const insertSelection = new Selection(selection.endLineNumber, selection.endColumn, selection.endLineNumber, selection.endColumn);154commands.push(new ReplaceCommandThatSelectsText(insertSelection, model.getValueInRange(selection)));155}156}157158editor.pushUndoStop();159editor.executeCommands(this.id, commands);160editor.pushUndoStop();161}162}163164// move lines165166abstract class AbstractMoveLinesAction extends EditorAction {167168private readonly down: boolean;169170constructor(down: boolean, opts: IActionOptions) {171super(opts);172this.down = down;173}174175public run(accessor: ServicesAccessor, editor: ICodeEditor): void {176const languageConfigurationService = accessor.get(ILanguageConfigurationService);177178const commands: ICommand[] = [];179const selections = editor.getSelections() || [];180const autoIndent = editor.getOption(EditorOption.autoIndent);181182for (const selection of selections) {183commands.push(new MoveLinesCommand(selection, this.down, autoIndent, languageConfigurationService));184}185186editor.pushUndoStop();187editor.executeCommands(this.id, commands);188editor.pushUndoStop();189}190}191192class MoveLinesUpAction extends AbstractMoveLinesAction {193constructor() {194super(false, {195id: 'editor.action.moveLinesUpAction',196label: nls.localize2('lines.moveUp', "Move Line Up"),197precondition: EditorContextKeys.writable,198kbOpts: {199kbExpr: EditorContextKeys.editorTextFocus,200primary: KeyMod.Alt | KeyCode.UpArrow,201linux: { primary: KeyMod.Alt | KeyCode.UpArrow },202weight: KeybindingWeight.EditorContrib203},204menuOpts: {205menuId: MenuId.MenubarSelectionMenu,206group: '2_line',207title: nls.localize({ key: 'miMoveLinesUp', comment: ['&& denotes a mnemonic'] }, "Mo&&ve Line Up"),208order: 3209},210canTriggerInlineEdits: true,211});212}213}214215class MoveLinesDownAction extends AbstractMoveLinesAction {216constructor() {217super(true, {218id: 'editor.action.moveLinesDownAction',219label: nls.localize2('lines.moveDown', "Move Line Down"),220precondition: EditorContextKeys.writable,221kbOpts: {222kbExpr: EditorContextKeys.editorTextFocus,223primary: KeyMod.Alt | KeyCode.DownArrow,224linux: { primary: KeyMod.Alt | KeyCode.DownArrow },225weight: KeybindingWeight.EditorContrib226},227menuOpts: {228menuId: MenuId.MenubarSelectionMenu,229group: '2_line',230title: nls.localize({ key: 'miMoveLinesDown', comment: ['&& denotes a mnemonic'] }, "Move &&Line Down"),231order: 4232},233canTriggerInlineEdits: true,234});235}236}237238export abstract class AbstractSortLinesAction extends EditorAction {239private readonly descending: boolean;240241constructor(descending: boolean, opts: IActionOptions) {242super(opts);243this.descending = descending;244}245246public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {247if (!editor.hasModel()) {248return;249}250251const model = editor.getModel();252let selections = editor.getSelections();253if (selections.length === 1 && selections[0].isSingleLine()) {254// Apply to whole document.255selections = [new Selection(1, 1, model.getLineCount(), model.getLineMaxColumn(model.getLineCount()))];256}257258for (const selection of selections) {259if (!SortLinesCommand.canRun(editor.getModel(), selection, this.descending)) {260return;261}262}263264const commands: ICommand[] = [];265for (let i = 0, len = selections.length; i < len; i++) {266commands[i] = new SortLinesCommand(selections[i], this.descending);267}268269editor.pushUndoStop();270editor.executeCommands(this.id, commands);271editor.pushUndoStop();272}273}274275export class SortLinesAscendingAction extends AbstractSortLinesAction {276constructor() {277super(false, {278id: 'editor.action.sortLinesAscending',279label: nls.localize2('lines.sortAscending', "Sort Lines Ascending"),280precondition: EditorContextKeys.writable,281canTriggerInlineEdits: true,282});283}284}285286export class SortLinesDescendingAction extends AbstractSortLinesAction {287constructor() {288super(true, {289id: 'editor.action.sortLinesDescending',290label: nls.localize2('lines.sortDescending', "Sort Lines Descending"),291precondition: EditorContextKeys.writable,292canTriggerInlineEdits: true,293});294}295}296297export class DeleteDuplicateLinesAction extends EditorAction {298constructor() {299super({300id: 'editor.action.removeDuplicateLines',301label: nls.localize2('lines.deleteDuplicates', "Delete Duplicate Lines"),302precondition: EditorContextKeys.writable,303canTriggerInlineEdits: true,304});305}306307public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {308if (!editor.hasModel()) {309return;310}311312const model: ITextModel = editor.getModel();313if (model.getLineCount() === 1 && model.getLineMaxColumn(1) === 1) {314return;315}316317const edits: ISingleEditOperation[] = [];318const endCursorState: Selection[] = [];319320let linesDeleted = 0;321let updateSelection = true;322323let selections = editor.getSelections();324if (selections.length === 1 && selections[0].isSingleLine()) {325// Apply to whole document.326selections = [new Selection(1, 1, model.getLineCount(), model.getLineMaxColumn(model.getLineCount()))];327updateSelection = false;328}329330for (const selection of selections) {331const uniqueLines = new Set();332const lines = [];333334for (let i = selection.startLineNumber; i <= selection.endLineNumber; i++) {335const line = model.getLineContent(i);336337if (uniqueLines.has(line)) {338continue;339}340341lines.push(line);342uniqueLines.add(line);343}344345346const selectionToReplace = new Selection(347selection.startLineNumber,3481,349selection.endLineNumber,350model.getLineMaxColumn(selection.endLineNumber)351);352353const adjustedSelectionStart = selection.startLineNumber - linesDeleted;354const finalSelection = new Selection(355adjustedSelectionStart,3561,357adjustedSelectionStart + lines.length - 1,358lines[lines.length - 1].length + 1359);360361edits.push(EditOperation.replace(selectionToReplace, lines.join('\n')));362endCursorState.push(finalSelection);363364linesDeleted += (selection.endLineNumber - selection.startLineNumber + 1) - lines.length;365}366367editor.pushUndoStop();368editor.executeEdits(this.id, edits, updateSelection ? endCursorState : undefined);369editor.pushUndoStop();370}371}372373export class ReverseLinesAction extends EditorAction {374constructor() {375super({376id: 'editor.action.reverseLines',377label: nls.localize2('lines.reverseLines', "Reverse lines"),378precondition: EditorContextKeys.writable,379canTriggerInlineEdits: true380});381}382383public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {384if (!editor.hasModel()) {385return;386}387388const model: ITextModel = editor.getModel();389const originalSelections = editor.getSelections();390let selections = originalSelections;391if (selections.length === 1 && selections[0].isSingleLine()) {392// Apply to whole document.393selections = [new Selection(1, 1, model.getLineCount(), model.getLineMaxColumn(model.getLineCount()))];394}395396const edits: ISingleEditOperation[] = [];397const resultingSelections: Selection[] = [];398399for (let i = 0; i < selections.length; i++) {400const selection = selections[i];401const originalSelection = originalSelections[i];402let endLineNumber = selection.endLineNumber;403if (selection.startLineNumber < selection.endLineNumber && selection.endColumn === 1) {404endLineNumber--;405}406407let range: Range = new Range(selection.startLineNumber, 1, endLineNumber, model.getLineMaxColumn(endLineNumber));408409// Exclude last line if empty and we're at the end of the document410if (endLineNumber === model.getLineCount() && model.getLineContent(range.endLineNumber) === '') {411range = range.setEndPosition(range.endLineNumber - 1, model.getLineMaxColumn(range.endLineNumber - 1));412}413414const lines: string[] = [];415for (let i = range.endLineNumber; i >= range.startLineNumber; i--) {416lines.push(model.getLineContent(i));417}418const edit: ISingleEditOperation = EditOperation.replace(range, lines.join('\n'));419edits.push(edit);420421const updateLineNumber = function (lineNumber: number): number {422return lineNumber <= range.endLineNumber ? range.endLineNumber - lineNumber + range.startLineNumber : lineNumber;423};424const updateSelection = function (sel: Selection): Selection {425if (sel.isEmpty()) {426// keep just the cursor427return new Selection(updateLineNumber(sel.positionLineNumber), sel.positionColumn, updateLineNumber(sel.positionLineNumber), sel.positionColumn);428} else {429// keep selection - maintain direction by creating backward selection430const newSelectionStart = updateLineNumber(sel.selectionStartLineNumber);431const newPosition = updateLineNumber(sel.positionLineNumber);432const newSelectionStartColumn = sel.selectionStartColumn;433const newPositionColumn = sel.positionColumn;434435// Create selection: from (newSelectionStart, newSelectionStartColumn) to (newPosition, newPositionColumn)436// After reversal: from (3, 2) to (1, 3)437return new Selection(newSelectionStart, newSelectionStartColumn, newPosition, newPositionColumn);438}439};440resultingSelections.push(updateSelection(originalSelection));441}442443editor.pushUndoStop();444editor.executeEdits(this.id, edits, resultingSelections);445editor.pushUndoStop();446}447}448449interface TrimTrailingWhitespaceArgs {450reason?: 'auto-save';451}452453export class TrimTrailingWhitespaceAction extends EditorAction {454455public static readonly ID = 'editor.action.trimTrailingWhitespace';456457constructor() {458super({459id: TrimTrailingWhitespaceAction.ID,460label: nls.localize2('lines.trimTrailingWhitespace', "Trim Trailing Whitespace"),461precondition: EditorContextKeys.writable,462kbOpts: {463kbExpr: EditorContextKeys.editorTextFocus,464primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyX),465weight: KeybindingWeight.EditorContrib466}467});468}469470public run(_accessor: ServicesAccessor, editor: ICodeEditor, args: TrimTrailingWhitespaceArgs): void {471472let cursors: Position[] = [];473if (args.reason === 'auto-save') {474// See https://github.com/editorconfig/editorconfig-vscode/issues/47475// It is very convenient for the editor config extension to invoke this action.476// So, if we get a reason:'auto-save' passed in, let's preserve cursor positions.477cursors = (editor.getSelections() || []).map(s => new Position(s.positionLineNumber, s.positionColumn));478}479480const selection = editor.getSelection();481if (selection === null) {482return;483}484485const config = _accessor.get(IConfigurationService);486const model = editor.getModel();487const trimInRegexAndStrings = config.getValue<boolean>('files.trimTrailingWhitespaceInRegexAndStrings', { overrideIdentifier: model?.getLanguageId(), resource: model?.uri });488489const command = new TrimTrailingWhitespaceCommand(selection, cursors, trimInRegexAndStrings);490491editor.pushUndoStop();492editor.executeCommands(this.id, [command]);493editor.pushUndoStop();494}495}496497// delete lines498499interface IDeleteLinesOperation {500startLineNumber: number;501selectionStartColumn: number;502endLineNumber: number;503positionColumn: number;504}505506export class DeleteLinesAction extends EditorAction {507508constructor() {509super({510id: 'editor.action.deleteLines',511label: nls.localize2('lines.delete', "Delete Line"),512precondition: EditorContextKeys.writable,513kbOpts: {514kbExpr: EditorContextKeys.textInputFocus,515primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyK,516weight: KeybindingWeight.EditorContrib517},518canTriggerInlineEdits: true,519});520}521522public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {523if (!editor.hasModel()) {524return;525}526527const ops = this._getLinesToRemove(editor);528529const model: ITextModel = editor.getModel();530if (model.getLineCount() === 1 && model.getLineMaxColumn(1) === 1) {531// Model is empty532return;533}534535let linesDeleted = 0;536const edits: ISingleEditOperation[] = [];537const cursorState: Selection[] = [];538for (let i = 0, len = ops.length; i < len; i++) {539const op = ops[i];540541let startLineNumber = op.startLineNumber;542let endLineNumber = op.endLineNumber;543544let startColumn = 1;545let endColumn = model.getLineMaxColumn(endLineNumber);546if (endLineNumber < model.getLineCount()) {547endLineNumber += 1;548endColumn = 1;549} else if (startLineNumber > 1) {550startLineNumber -= 1;551startColumn = model.getLineMaxColumn(startLineNumber);552}553554edits.push(EditOperation.replace(new Selection(startLineNumber, startColumn, endLineNumber, endColumn), ''));555cursorState.push(new Selection(startLineNumber - linesDeleted, op.positionColumn, startLineNumber - linesDeleted, op.positionColumn));556linesDeleted += (op.endLineNumber - op.startLineNumber + 1);557}558559editor.pushUndoStop();560editor.executeEdits(this.id, edits, cursorState);561editor.revealAllCursors(true);562editor.pushUndoStop();563}564565private _getLinesToRemove(editor: IActiveCodeEditor): IDeleteLinesOperation[] {566// Construct delete operations567const operations: IDeleteLinesOperation[] = editor.getSelections().map((s) => {568569let endLineNumber = s.endLineNumber;570if (s.startLineNumber < s.endLineNumber && s.endColumn === 1) {571endLineNumber -= 1;572}573574return {575startLineNumber: s.startLineNumber,576selectionStartColumn: s.selectionStartColumn,577endLineNumber: endLineNumber,578positionColumn: s.positionColumn579};580});581582// Sort delete operations583operations.sort((a, b) => {584if (a.startLineNumber === b.startLineNumber) {585return a.endLineNumber - b.endLineNumber;586}587return a.startLineNumber - b.startLineNumber;588});589590// Merge delete operations which are adjacent or overlapping591const mergedOperations: IDeleteLinesOperation[] = [];592let previousOperation = operations[0];593for (let i = 1; i < operations.length; i++) {594if (previousOperation.endLineNumber + 1 >= operations[i].startLineNumber) {595// Merge current operations into the previous one596previousOperation.endLineNumber = operations[i].endLineNumber;597} else {598// Push previous operation599mergedOperations.push(previousOperation);600previousOperation = operations[i];601}602}603// Push the last operation604mergedOperations.push(previousOperation);605606return mergedOperations;607}608}609610export class IndentLinesAction extends EditorAction {611constructor() {612super({613id: 'editor.action.indentLines',614label: nls.localize2('lines.indent', "Indent Line"),615precondition: EditorContextKeys.writable,616kbOpts: {617kbExpr: EditorContextKeys.editorTextFocus,618primary: KeyMod.CtrlCmd | KeyCode.BracketRight,619weight: KeybindingWeight.EditorContrib620},621canTriggerInlineEdits: true,622});623}624625public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {626const viewModel = editor._getViewModel();627if (!viewModel) {628return;629}630editor.pushUndoStop();631editor.executeCommands(this.id, TypeOperations.indent(viewModel.cursorConfig, editor.getModel(), editor.getSelections()));632editor.pushUndoStop();633}634}635636class OutdentLinesAction extends EditorAction {637constructor() {638super({639id: 'editor.action.outdentLines',640label: nls.localize2('lines.outdent', "Outdent Line"),641precondition: EditorContextKeys.writable,642kbOpts: {643kbExpr: EditorContextKeys.editorTextFocus,644primary: KeyMod.CtrlCmd | KeyCode.BracketLeft,645weight: KeybindingWeight.EditorContrib646},647canTriggerInlineEdits: true,648});649}650651public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {652CoreEditingCommands.Outdent.runEditorCommand(_accessor, editor, null);653}654}655656export class InsertLineBeforeAction extends EditorAction {657public static readonly ID = 'editor.action.insertLineBefore';658constructor() {659super({660id: InsertLineBeforeAction.ID,661label: nls.localize2('lines.insertBefore', "Insert Line Above"),662precondition: EditorContextKeys.writable,663kbOpts: {664kbExpr: EditorContextKeys.editorTextFocus,665primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter,666weight: KeybindingWeight.EditorContrib667},668canTriggerInlineEdits: true,669});670}671672public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {673const viewModel = editor._getViewModel();674if (!viewModel) {675return;676}677editor.pushUndoStop();678editor.executeCommands(this.id, EnterOperation.lineInsertBefore(viewModel.cursorConfig, editor.getModel(), editor.getSelections()));679}680}681682export class InsertLineAfterAction extends EditorAction {683public static readonly ID = 'editor.action.insertLineAfter';684constructor() {685super({686id: InsertLineAfterAction.ID,687label: nls.localize2('lines.insertAfter', "Insert Line Below"),688precondition: EditorContextKeys.writable,689kbOpts: {690kbExpr: EditorContextKeys.editorTextFocus,691primary: KeyMod.CtrlCmd | KeyCode.Enter,692weight: KeybindingWeight.EditorContrib693},694canTriggerInlineEdits: true,695});696}697698public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {699const viewModel = editor._getViewModel();700if (!viewModel) {701return;702}703editor.pushUndoStop();704editor.executeCommands(this.id, EnterOperation.lineInsertAfter(viewModel.cursorConfig, editor.getModel(), editor.getSelections()));705}706}707708export abstract class AbstractDeleteAllToBoundaryAction extends EditorAction {709public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {710if (!editor.hasModel()) {711return;712}713const primaryCursor = editor.getSelection();714715const rangesToDelete = this._getRangesToDelete(editor);716// merge overlapping selections717const effectiveRanges: Range[] = [];718719for (let i = 0, count = rangesToDelete.length - 1; i < count; i++) {720const range = rangesToDelete[i];721const nextRange = rangesToDelete[i + 1];722723if (Range.intersectRanges(range, nextRange) === null) {724effectiveRanges.push(range);725} else {726rangesToDelete[i + 1] = Range.plusRange(range, nextRange);727}728}729730effectiveRanges.push(rangesToDelete[rangesToDelete.length - 1]);731732const endCursorState = this._getEndCursorState(primaryCursor, effectiveRanges);733734const edits: ISingleEditOperation[] = effectiveRanges.map(range => {735return EditOperation.replace(range, '');736});737738editor.pushUndoStop();739editor.executeEdits(this.id, edits, endCursorState);740editor.pushUndoStop();741}742743/**744* Compute the cursor state after the edit operations were applied.745*/746protected abstract _getEndCursorState(primaryCursor: Range, rangesToDelete: Range[]): Selection[];747748protected abstract _getRangesToDelete(editor: IActiveCodeEditor): Range[];749}750751export class DeleteAllLeftAction extends AbstractDeleteAllToBoundaryAction {752constructor() {753super({754id: 'deleteAllLeft',755label: nls.localize2('lines.deleteAllLeft', "Delete All Left"),756precondition: EditorContextKeys.writable,757kbOpts: {758kbExpr: EditorContextKeys.textInputFocus,759primary: 0,760mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace },761weight: KeybindingWeight.EditorContrib762},763canTriggerInlineEdits: true,764});765}766767protected _getEndCursorState(primaryCursor: Range, rangesToDelete: Range[]): Selection[] {768let endPrimaryCursor: Selection | null = null;769const endCursorState: Selection[] = [];770let deletedLines = 0;771772rangesToDelete.forEach(range => {773let endCursor;774if (range.endColumn === 1 && deletedLines > 0) {775const newStartLine = range.startLineNumber - deletedLines;776endCursor = new Selection(newStartLine, range.startColumn, newStartLine, range.startColumn);777} else {778endCursor = new Selection(range.startLineNumber, range.startColumn, range.startLineNumber, range.startColumn);779}780781deletedLines += range.endLineNumber - range.startLineNumber;782783if (range.intersectRanges(primaryCursor)) {784endPrimaryCursor = endCursor;785} else {786endCursorState.push(endCursor);787}788});789790if (endPrimaryCursor) {791endCursorState.unshift(endPrimaryCursor);792}793794return endCursorState;795}796797protected _getRangesToDelete(editor: IActiveCodeEditor): Range[] {798const selections = editor.getSelections();799if (selections === null) {800return [];801}802803let rangesToDelete: Range[] = selections;804const model = editor.getModel();805806if (model === null) {807return [];808}809810rangesToDelete.sort(Range.compareRangesUsingStarts);811rangesToDelete = rangesToDelete.map(selection => {812if (selection.isEmpty()) {813if (selection.startColumn === 1) {814const deleteFromLine = Math.max(1, selection.startLineNumber - 1);815const deleteFromColumn = selection.startLineNumber === 1 ? 1 : model.getLineLength(deleteFromLine) + 1;816return new Range(deleteFromLine, deleteFromColumn, selection.startLineNumber, 1);817} else {818return new Range(selection.startLineNumber, 1, selection.startLineNumber, selection.startColumn);819}820} else {821return new Range(selection.startLineNumber, 1, selection.endLineNumber, selection.endColumn);822}823});824825return rangesToDelete;826}827}828829export class DeleteAllRightAction extends AbstractDeleteAllToBoundaryAction {830constructor() {831super({832id: 'deleteAllRight',833label: nls.localize2('lines.deleteAllRight', "Delete All Right"),834precondition: EditorContextKeys.writable,835kbOpts: {836kbExpr: EditorContextKeys.textInputFocus,837primary: 0,838mac: { primary: KeyMod.WinCtrl | KeyCode.KeyK, secondary: [KeyMod.CtrlCmd | KeyCode.Delete] },839weight: KeybindingWeight.EditorContrib840},841canTriggerInlineEdits: true,842});843}844845protected _getEndCursorState(primaryCursor: Range, rangesToDelete: Range[]): Selection[] {846let endPrimaryCursor: Selection | null = null;847const endCursorState: Selection[] = [];848for (let i = 0, len = rangesToDelete.length, offset = 0; i < len; i++) {849const range = rangesToDelete[i];850const endCursor = new Selection(range.startLineNumber - offset, range.startColumn, range.startLineNumber - offset, range.startColumn);851852if (range.intersectRanges(primaryCursor)) {853endPrimaryCursor = endCursor;854} else {855endCursorState.push(endCursor);856}857}858859if (endPrimaryCursor) {860endCursorState.unshift(endPrimaryCursor);861}862863return endCursorState;864}865866protected _getRangesToDelete(editor: IActiveCodeEditor): Range[] {867const model = editor.getModel();868if (model === null) {869return [];870}871872const selections = editor.getSelections();873874if (selections === null) {875return [];876}877878const rangesToDelete: Range[] = selections.map((sel) => {879if (sel.isEmpty()) {880const maxColumn = model.getLineMaxColumn(sel.startLineNumber);881882if (sel.startColumn === maxColumn) {883return new Range(sel.startLineNumber, sel.startColumn, sel.startLineNumber + 1, 1);884} else {885return new Range(sel.startLineNumber, sel.startColumn, sel.startLineNumber, maxColumn);886}887}888return sel;889});890891rangesToDelete.sort(Range.compareRangesUsingStarts);892return rangesToDelete;893}894}895896export class JoinLinesAction extends EditorAction {897constructor() {898super({899id: 'editor.action.joinLines',900label: nls.localize2('lines.joinLines', "Join Lines"),901precondition: EditorContextKeys.writable,902kbOpts: {903kbExpr: EditorContextKeys.editorTextFocus,904primary: 0,905mac: { primary: KeyMod.WinCtrl | KeyCode.KeyJ },906weight: KeybindingWeight.EditorContrib907},908canTriggerInlineEdits: true,909});910}911912public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {913const selections = editor.getSelections();914if (selections === null) {915return;916}917918let primaryCursor = editor.getSelection();919if (primaryCursor === null) {920return;921}922923selections.sort(Range.compareRangesUsingStarts);924const reducedSelections: Selection[] = [];925926const lastSelection = selections.reduce((previousValue, currentValue) => {927if (previousValue.isEmpty()) {928if (previousValue.endLineNumber === currentValue.startLineNumber) {929if (primaryCursor!.equalsSelection(previousValue)) {930primaryCursor = currentValue;931}932return currentValue;933}934935if (currentValue.startLineNumber > previousValue.endLineNumber + 1) {936reducedSelections.push(previousValue);937return currentValue;938} else {939return new Selection(previousValue.startLineNumber, previousValue.startColumn, currentValue.endLineNumber, currentValue.endColumn);940}941} else {942if (currentValue.startLineNumber > previousValue.endLineNumber) {943reducedSelections.push(previousValue);944return currentValue;945} else {946return new Selection(previousValue.startLineNumber, previousValue.startColumn, currentValue.endLineNumber, currentValue.endColumn);947}948}949});950951reducedSelections.push(lastSelection);952953const model = editor.getModel();954if (model === null) {955return;956}957958const edits: ISingleEditOperation[] = [];959const endCursorState: Selection[] = [];960let endPrimaryCursor = primaryCursor;961let lineOffset = 0;962963for (let i = 0, len = reducedSelections.length; i < len; i++) {964const selection = reducedSelections[i];965const startLineNumber = selection.startLineNumber;966const startColumn = 1;967let columnDeltaOffset = 0;968let endLineNumber: number,969endColumn: number;970971const selectionEndPositionOffset = model.getLineLength(selection.endLineNumber) - selection.endColumn;972973if (selection.isEmpty() || selection.startLineNumber === selection.endLineNumber) {974const position = selection.getStartPosition();975if (position.lineNumber < model.getLineCount()) {976endLineNumber = startLineNumber + 1;977endColumn = model.getLineMaxColumn(endLineNumber);978} else {979endLineNumber = position.lineNumber;980endColumn = model.getLineMaxColumn(position.lineNumber);981}982} else {983endLineNumber = selection.endLineNumber;984endColumn = model.getLineMaxColumn(endLineNumber);985}986987let trimmedLinesContent = model.getLineContent(startLineNumber);988989for (let i = startLineNumber + 1; i <= endLineNumber; i++) {990const lineText = model.getLineContent(i);991const firstNonWhitespaceIdx = model.getLineFirstNonWhitespaceColumn(i);992993if (firstNonWhitespaceIdx >= 1) {994let insertSpace = true;995if (trimmedLinesContent === '') {996insertSpace = false;997}998999if (insertSpace && (trimmedLinesContent.charAt(trimmedLinesContent.length - 1) === ' ' ||1000trimmedLinesContent.charAt(trimmedLinesContent.length - 1) === '\t')) {1001insertSpace = false;1002trimmedLinesContent = trimmedLinesContent.replace(/[\s\uFEFF\xA0]+$/g, ' ');1003}10041005const lineTextWithoutIndent = lineText.substr(firstNonWhitespaceIdx - 1);10061007trimmedLinesContent += (insertSpace ? ' ' : '') + lineTextWithoutIndent;10081009if (insertSpace) {1010columnDeltaOffset = lineTextWithoutIndent.length + 1;1011} else {1012columnDeltaOffset = lineTextWithoutIndent.length;1013}1014} else {1015columnDeltaOffset = 0;1016}1017}10181019const deleteSelection = new Range(startLineNumber, startColumn, endLineNumber, endColumn);10201021if (!deleteSelection.isEmpty()) {1022let resultSelection: Selection;10231024if (selection.isEmpty()) {1025edits.push(EditOperation.replace(deleteSelection, trimmedLinesContent));1026resultSelection = new Selection(deleteSelection.startLineNumber - lineOffset, trimmedLinesContent.length - columnDeltaOffset + 1, startLineNumber - lineOffset, trimmedLinesContent.length - columnDeltaOffset + 1);1027} else {1028if (selection.startLineNumber === selection.endLineNumber) {1029edits.push(EditOperation.replace(deleteSelection, trimmedLinesContent));1030resultSelection = new Selection(selection.startLineNumber - lineOffset, selection.startColumn,1031selection.endLineNumber - lineOffset, selection.endColumn);1032} else {1033edits.push(EditOperation.replace(deleteSelection, trimmedLinesContent));1034resultSelection = new Selection(selection.startLineNumber - lineOffset, selection.startColumn,1035selection.startLineNumber - lineOffset, trimmedLinesContent.length - selectionEndPositionOffset);1036}1037}10381039if (Range.intersectRanges(deleteSelection, primaryCursor) !== null) {1040endPrimaryCursor = resultSelection;1041} else {1042endCursorState.push(resultSelection);1043}1044}10451046lineOffset += deleteSelection.endLineNumber - deleteSelection.startLineNumber;1047}10481049endCursorState.unshift(endPrimaryCursor);1050editor.pushUndoStop();1051editor.executeEdits(this.id, edits, endCursorState);1052editor.pushUndoStop();1053}1054}10551056export class TransposeAction extends EditorAction {1057constructor() {1058super({1059id: 'editor.action.transpose',1060label: nls.localize2('editor.transpose', "Transpose Characters around the Cursor"),1061precondition: EditorContextKeys.writable,1062canTriggerInlineEdits: true,1063});1064}10651066public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {1067const selections = editor.getSelections();1068if (selections === null) {1069return;1070}10711072const model = editor.getModel();1073if (model === null) {1074return;1075}10761077const commands: ICommand[] = [];10781079for (let i = 0, len = selections.length; i < len; i++) {1080const selection = selections[i];10811082if (!selection.isEmpty()) {1083continue;1084}10851086const cursor = selection.getStartPosition();1087const maxColumn = model.getLineMaxColumn(cursor.lineNumber);10881089if (cursor.column >= maxColumn) {1090if (cursor.lineNumber === model.getLineCount()) {1091continue;1092}10931094// The cursor is at the end of current line and current line is not empty1095// then we transpose the character before the cursor and the line break if there is any following line.1096const deleteSelection = new Range(cursor.lineNumber, Math.max(1, cursor.column - 1), cursor.lineNumber + 1, 1);1097const chars = model.getValueInRange(deleteSelection).split('').reverse().join('');10981099commands.push(new ReplaceCommand(new Selection(cursor.lineNumber, Math.max(1, cursor.column - 1), cursor.lineNumber + 1, 1), chars));1100} else {1101const deleteSelection = new Range(cursor.lineNumber, Math.max(1, cursor.column - 1), cursor.lineNumber, cursor.column + 1);1102const chars = model.getValueInRange(deleteSelection).split('').reverse().join('');1103commands.push(new ReplaceCommandThatPreservesSelection(deleteSelection, chars,1104new Selection(cursor.lineNumber, cursor.column + 1, cursor.lineNumber, cursor.column + 1)));1105}1106}11071108editor.pushUndoStop();1109editor.executeCommands(this.id, commands);1110editor.pushUndoStop();1111}1112}11131114export abstract class AbstractCaseAction extends EditorAction {1115public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {1116const selections = editor.getSelections();1117if (selections === null) {1118return;1119}11201121const model = editor.getModel();1122if (model === null) {1123return;1124}11251126const wordSeparators = editor.getOption(EditorOption.wordSeparators);1127const textEdits: ISingleEditOperation[] = [];11281129for (const selection of selections) {1130if (selection.isEmpty()) {1131const cursor = selection.getStartPosition();1132const word = editor.getConfiguredWordAtPosition(cursor);11331134if (!word) {1135continue;1136}11371138const wordRange = new Range(cursor.lineNumber, word.startColumn, cursor.lineNumber, word.endColumn);1139const text = model.getValueInRange(wordRange);1140textEdits.push(EditOperation.replace(wordRange, this._modifyText(text, wordSeparators)));1141} else {1142const text = model.getValueInRange(selection);1143textEdits.push(EditOperation.replace(selection, this._modifyText(text, wordSeparators)));1144}1145}11461147editor.pushUndoStop();1148editor.executeEdits(this.id, textEdits);1149editor.pushUndoStop();1150}11511152protected abstract _modifyText(text: string, wordSeparators: string): string;1153}11541155export class UpperCaseAction extends AbstractCaseAction {1156constructor() {1157super({1158id: 'editor.action.transformToUppercase',1159label: nls.localize2('editor.transformToUppercase', "Transform to Uppercase"),1160precondition: EditorContextKeys.writable,1161canTriggerInlineEdits: true,1162});1163}11641165protected _modifyText(text: string, wordSeparators: string): string {1166return text.toLocaleUpperCase();1167}1168}11691170export class LowerCaseAction extends AbstractCaseAction {1171constructor() {1172super({1173id: 'editor.action.transformToLowercase',1174label: nls.localize2('editor.transformToLowercase', "Transform to Lowercase"),1175precondition: EditorContextKeys.writable,1176canTriggerInlineEdits: true1177});1178}11791180protected _modifyText(text: string, wordSeparators: string): string {1181return text.toLocaleLowerCase();1182}1183}11841185class BackwardsCompatibleRegExp {11861187private _actual: RegExp | null;1188private _evaluated: boolean;11891190constructor(1191private readonly _pattern: string,1192private readonly _flags: string1193) {1194this._actual = null;1195this._evaluated = false;1196}11971198public get(): RegExp | null {1199if (!this._evaluated) {1200this._evaluated = true;1201try {1202this._actual = new RegExp(this._pattern, this._flags);1203} catch (err) {1204// this browser does not support this regular expression1205}1206}1207return this._actual;1208}12091210public isSupported(): boolean {1211return (this.get() !== null);1212}1213}12141215export class TitleCaseAction extends AbstractCaseAction {12161217public static titleBoundary = new BackwardsCompatibleRegExp('(^|[^\\p{L}\\p{N}\']|((^|\\P{L})\'))\\p{L}', 'gmu');12181219constructor() {1220super({1221id: 'editor.action.transformToTitlecase',1222label: nls.localize2('editor.transformToTitlecase', "Transform to Title Case"),1223precondition: EditorContextKeys.writable,1224canTriggerInlineEdits: true1225});1226}12271228protected _modifyText(text: string, wordSeparators: string): string {1229const titleBoundary = TitleCaseAction.titleBoundary.get();1230if (!titleBoundary) {1231// cannot support this1232return text;1233}1234return text1235.toLocaleLowerCase()1236.replace(titleBoundary, (b) => b.toLocaleUpperCase());1237}1238}12391240export class SnakeCaseAction extends AbstractCaseAction {12411242public static caseBoundary = new BackwardsCompatibleRegExp('(\\p{Ll})(\\p{Lu})', 'gmu');1243public static singleLetters = new BackwardsCompatibleRegExp('(\\p{Lu}|\\p{N})(\\p{Lu})(\\p{Ll})', 'gmu');12441245constructor() {1246super({1247id: 'editor.action.transformToSnakecase',1248label: nls.localize2('editor.transformToSnakecase', "Transform to Snake Case"),1249precondition: EditorContextKeys.writable,1250canTriggerInlineEdits: true,1251});1252}12531254protected _modifyText(text: string, wordSeparators: string): string {1255const caseBoundary = SnakeCaseAction.caseBoundary.get();1256const singleLetters = SnakeCaseAction.singleLetters.get();1257if (!caseBoundary || !singleLetters) {1258// cannot support this1259return text;1260}1261return (text1262.replace(caseBoundary, '$1_$2')1263.replace(singleLetters, '$1_$2$3')1264.toLocaleLowerCase()1265);1266}1267}12681269export class CamelCaseAction extends AbstractCaseAction {1270public static singleLineWordBoundary = new BackwardsCompatibleRegExp('[_\\s-]+', 'gm');1271public static multiLineWordBoundary = new BackwardsCompatibleRegExp('[_-]+', 'gm');1272public static validWordStart = new BackwardsCompatibleRegExp('^(\\p{Lu}[^\\p{Lu}])', 'gmu');12731274constructor() {1275super({1276id: 'editor.action.transformToCamelcase',1277label: nls.localize2('editor.transformToCamelcase', "Transform to Camel Case"),1278precondition: EditorContextKeys.writable,1279canTriggerInlineEdits: true1280});1281}12821283protected _modifyText(text: string, wordSeparators: string): string {1284const wordBoundary = /\r\n|\r|\n/.test(text) ? CamelCaseAction.multiLineWordBoundary.get() : CamelCaseAction.singleLineWordBoundary.get();1285const validWordStart = CamelCaseAction.validWordStart.get();1286if (!wordBoundary || !validWordStart) {1287// cannot support this1288return text;1289}1290const words = text.split(wordBoundary);1291const firstWord = words.shift()?.replace(validWordStart, (start: string) => start.toLocaleLowerCase());1292return firstWord + words.map((word: string) => word.substring(0, 1).toLocaleUpperCase() + word.substring(1))1293.join('');1294}1295}12961297export class PascalCaseAction extends AbstractCaseAction {1298public static wordBoundary = new BackwardsCompatibleRegExp('[_ \\t-]', 'gm');1299public static wordBoundaryToMaintain = new BackwardsCompatibleRegExp('(?<=\\.)', 'gm');1300public static upperCaseWordMatcher = new BackwardsCompatibleRegExp('^\\p{Lu}+$', 'mu');13011302constructor() {1303super({1304id: 'editor.action.transformToPascalcase',1305label: nls.localize2('editor.transformToPascalcase', "Transform to Pascal Case"),1306precondition: EditorContextKeys.writable,1307canTriggerInlineEdits: true,1308});1309}13101311protected _modifyText(text: string, wordSeparators: string): string {1312const wordBoundary = PascalCaseAction.wordBoundary.get();1313const wordBoundaryToMaintain = PascalCaseAction.wordBoundaryToMaintain.get();1314const upperCaseWordMatcher = PascalCaseAction.upperCaseWordMatcher.get();13151316if (!wordBoundary || !wordBoundaryToMaintain || !upperCaseWordMatcher) {1317// cannot support this1318return text;1319}13201321const wordsWithMaintainBoundaries = text.split(wordBoundaryToMaintain);1322const words = wordsWithMaintainBoundaries.map(word => word.split(wordBoundary)).flat();13231324return words.map(word => {1325const normalizedWord = word.charAt(0).toLocaleUpperCase() + word.slice(1);1326const isAllCaps = normalizedWord.length > 1 && upperCaseWordMatcher.test(normalizedWord);1327if (isAllCaps) {1328return normalizedWord.charAt(0) + normalizedWord.slice(1).toLocaleLowerCase();1329}1330return normalizedWord;1331}).join('');1332}1333}13341335export class KebabCaseAction extends AbstractCaseAction {13361337public static isSupported(): boolean {1338const areAllRegexpsSupported = [1339this.caseBoundary,1340this.singleLetters,1341this.underscoreBoundary,1342].every((regexp) => regexp.isSupported());13431344return areAllRegexpsSupported;1345}13461347private static caseBoundary = new BackwardsCompatibleRegExp('(\\p{Ll})(\\p{Lu})', 'gmu');1348private static singleLetters = new BackwardsCompatibleRegExp('(\\p{Lu}|\\p{N})(\\p{Lu}\\p{Ll})', 'gmu');1349private static underscoreBoundary = new BackwardsCompatibleRegExp('(\\S)(_)(\\S)', 'gm');13501351constructor() {1352super({1353id: 'editor.action.transformToKebabcase',1354label: nls.localize2('editor.transformToKebabcase', 'Transform to Kebab Case'),1355precondition: EditorContextKeys.writable,1356canTriggerInlineEdits: true,1357});1358}13591360protected _modifyText(text: string, _: string): string {1361const caseBoundary = KebabCaseAction.caseBoundary.get();1362const singleLetters = KebabCaseAction.singleLetters.get();1363const underscoreBoundary = KebabCaseAction.underscoreBoundary.get();13641365if (!caseBoundary || !singleLetters || !underscoreBoundary) {1366// one or more regexps aren't supported1367return text;1368}13691370return text1371.replace(underscoreBoundary, '$1-$3')1372.replace(caseBoundary, '$1-$2')1373.replace(singleLetters, '$1-$2')1374.toLocaleLowerCase();1375}1376}13771378registerEditorAction(CopyLinesUpAction);1379registerEditorAction(CopyLinesDownAction);1380registerEditorAction(DuplicateSelectionAction);1381registerEditorAction(MoveLinesUpAction);1382registerEditorAction(MoveLinesDownAction);1383registerEditorAction(SortLinesAscendingAction);1384registerEditorAction(SortLinesDescendingAction);1385registerEditorAction(DeleteDuplicateLinesAction);1386registerEditorAction(TrimTrailingWhitespaceAction);1387registerEditorAction(DeleteLinesAction);1388registerEditorAction(IndentLinesAction);1389registerEditorAction(OutdentLinesAction);1390registerEditorAction(InsertLineBeforeAction);1391registerEditorAction(InsertLineAfterAction);1392registerEditorAction(DeleteAllLeftAction);1393registerEditorAction(DeleteAllRightAction);1394registerEditorAction(JoinLinesAction);1395registerEditorAction(TransposeAction);1396registerEditorAction(UpperCaseAction);1397registerEditorAction(LowerCaseAction);1398registerEditorAction(ReverseLinesAction);13991400if (SnakeCaseAction.caseBoundary.isSupported() && SnakeCaseAction.singleLetters.isSupported()) {1401registerEditorAction(SnakeCaseAction);1402}1403if (CamelCaseAction.singleLineWordBoundary.isSupported() && CamelCaseAction.multiLineWordBoundary.isSupported()) {1404registerEditorAction(CamelCaseAction);1405}1406if (PascalCaseAction.wordBoundary.isSupported()) {1407registerEditorAction(PascalCaseAction);1408}1409if (TitleCaseAction.titleBoundary.isSupported()) {1410registerEditorAction(TitleCaseAction);1411}14121413if (KebabCaseAction.isSupported()) {1414registerEditorAction(KebabCaseAction);1415}141614171418