Path: blob/main/src/vs/editor/common/cursor/cursorDeleteOperations.ts
3294 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 * as strings from '../../../base/common/strings.js';6import { ReplaceCommand } from '../commands/replaceCommand.js';7import { EditorAutoClosingEditStrategy, EditorAutoClosingStrategy } from '../config/editorOptions.js';8import { CursorConfiguration, EditOperationResult, EditOperationType, ICursorSimpleModel, isQuote } from '../cursorCommon.js';9import { CursorColumns } from '../core/cursorColumns.js';10import { MoveOperations } from './cursorMoveOperations.js';11import { Range } from '../core/range.js';12import { Selection } from '../core/selection.js';13import { ICommand } from '../editorCommon.js';14import { StandardAutoClosingPairConditional } from '../languages/languageConfiguration.js';15import { Position } from '../core/position.js';1617export class DeleteOperations {1819public static deleteRight(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[]): [boolean, Array<ICommand | null>] {20const commands: Array<ICommand | null> = [];21let shouldPushStackElementBefore = (prevEditOperationType !== EditOperationType.DeletingRight);22for (let i = 0, len = selections.length; i < len; i++) {23const selection = selections[i];2425const deleteSelection = this.getDeleteRightRange(selection, model, config);2627if (deleteSelection.isEmpty()) {28// Probably at end of file => ignore29commands[i] = null;30continue;31}3233if (deleteSelection.startLineNumber !== deleteSelection.endLineNumber) {34shouldPushStackElementBefore = true;35}3637commands[i] = new ReplaceCommand(deleteSelection, '');38}39return [shouldPushStackElementBefore, commands];40}4142private static getDeleteRightRange(selection: Selection, model: ICursorSimpleModel, config: CursorConfiguration): Range {43if (!selection.isEmpty()) {44return selection;45}4647const position = selection.getPosition();48const rightOfPosition = MoveOperations.right(config, model, position);4950if (config.trimWhitespaceOnDelete && rightOfPosition.lineNumber !== position.lineNumber) {51// Smart line join (deleting leading whitespace) is on52// (and) Delete is happening at the end of a line53const currentLineHasContent = (model.getLineFirstNonWhitespaceColumn(position.lineNumber) > 0);54const firstNonWhitespaceColumn = model.getLineFirstNonWhitespaceColumn(rightOfPosition.lineNumber);55if (currentLineHasContent && firstNonWhitespaceColumn > 0) {56// The next line has content57return new Range(58rightOfPosition.lineNumber,59firstNonWhitespaceColumn,60position.lineNumber,61position.column62);63}64}6566return new Range(67rightOfPosition.lineNumber,68rightOfPosition.column,69position.lineNumber,70position.column71);72}7374public static isAutoClosingPairDelete(75autoClosingDelete: EditorAutoClosingEditStrategy,76autoClosingBrackets: EditorAutoClosingStrategy,77autoClosingQuotes: EditorAutoClosingStrategy,78autoClosingPairsOpen: Map<string, StandardAutoClosingPairConditional[]>,79model: ICursorSimpleModel,80selections: Selection[],81autoClosedCharacters: Range[]82): boolean {83if (autoClosingBrackets === 'never' && autoClosingQuotes === 'never') {84return false;85}86if (autoClosingDelete === 'never') {87return false;88}8990for (let i = 0, len = selections.length; i < len; i++) {91const selection = selections[i];92const position = selection.getPosition();9394if (!selection.isEmpty()) {95return false;96}9798const lineText = model.getLineContent(position.lineNumber);99if (position.column < 2 || position.column >= lineText.length + 1) {100return false;101}102const character = lineText.charAt(position.column - 2);103104const autoClosingPairCandidates = autoClosingPairsOpen.get(character);105if (!autoClosingPairCandidates) {106return false;107}108109if (isQuote(character)) {110if (autoClosingQuotes === 'never') {111return false;112}113} else {114if (autoClosingBrackets === 'never') {115return false;116}117}118119const afterCharacter = lineText.charAt(position.column - 1);120121let foundAutoClosingPair = false;122for (const autoClosingPairCandidate of autoClosingPairCandidates) {123if (autoClosingPairCandidate.open === character && autoClosingPairCandidate.close === afterCharacter) {124foundAutoClosingPair = true;125}126}127if (!foundAutoClosingPair) {128return false;129}130131// Must delete the pair only if it was automatically inserted by the editor132if (autoClosingDelete === 'auto') {133let found = false;134for (let j = 0, lenJ = autoClosedCharacters.length; j < lenJ; j++) {135const autoClosedCharacter = autoClosedCharacters[j];136if (position.lineNumber === autoClosedCharacter.startLineNumber && position.column === autoClosedCharacter.startColumn) {137found = true;138break;139}140}141if (!found) {142return false;143}144}145}146147return true;148}149150private static _runAutoClosingPairDelete(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[]): [boolean, ICommand[]] {151const commands: ICommand[] = [];152for (let i = 0, len = selections.length; i < len; i++) {153const position = selections[i].getPosition();154const deleteSelection = new Range(155position.lineNumber,156position.column - 1,157position.lineNumber,158position.column + 1159);160commands[i] = new ReplaceCommand(deleteSelection, '');161}162return [true, commands];163}164165public static deleteLeft(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[], autoClosedCharacters: Range[]): [boolean, Array<ICommand | null>] {166if (this.isAutoClosingPairDelete(config.autoClosingDelete, config.autoClosingBrackets, config.autoClosingQuotes, config.autoClosingPairs.autoClosingPairsOpenByEnd, model, selections, autoClosedCharacters)) {167return this._runAutoClosingPairDelete(config, model, selections);168}169170const commands: Array<ICommand | null> = [];171let shouldPushStackElementBefore = (prevEditOperationType !== EditOperationType.DeletingLeft);172for (let i = 0, len = selections.length; i < len; i++) {173const deleteRange = DeleteOperations.getDeleteLeftRange(selections[i], model, config);174175// Ignore empty delete ranges, as they have no effect176// They happen if the cursor is at the beginning of the file.177if (deleteRange.isEmpty()) {178commands[i] = null;179continue;180}181182if (deleteRange.startLineNumber !== deleteRange.endLineNumber) {183shouldPushStackElementBefore = true;184}185186commands[i] = new ReplaceCommand(deleteRange, '');187}188return [shouldPushStackElementBefore, commands];189190}191192private static getDeleteLeftRange(selection: Selection, model: ICursorSimpleModel, config: CursorConfiguration): Range {193if (!selection.isEmpty()) {194return selection;195}196197const position = selection.getPosition();198199// Unintend when using tab stops and cursor is within indentation200if (config.useTabStops && position.column > 1) {201const lineContent = model.getLineContent(position.lineNumber);202203const firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent);204const lastIndentationColumn = (205firstNonWhitespaceIndex === -1206? /* entire string is whitespace */ lineContent.length + 1207: firstNonWhitespaceIndex + 1208);209210if (position.column <= lastIndentationColumn) {211const fromVisibleColumn = config.visibleColumnFromColumn(model, position);212const toVisibleColumn = CursorColumns.prevIndentTabStop(fromVisibleColumn, config.indentSize);213const toColumn = config.columnFromVisibleColumn(model, position.lineNumber, toVisibleColumn);214return new Range(position.lineNumber, toColumn, position.lineNumber, position.column);215}216}217218return Range.fromPositions(DeleteOperations.getPositionAfterDeleteLeft(position, model), position);219}220221private static getPositionAfterDeleteLeft(position: Position, model: ICursorSimpleModel): Position {222if (position.column > 1) {223// Convert 1-based columns to 0-based offsets and back.224const idx = strings.getLeftDeleteOffset(position.column - 1, model.getLineContent(position.lineNumber));225return position.with(undefined, idx + 1);226} else if (position.lineNumber > 1) {227const newLine = position.lineNumber - 1;228return new Position(newLine, model.getLineMaxColumn(newLine));229} else {230return position;231}232}233234public static cut(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[]): EditOperationResult {235const commands: Array<ICommand | null> = [];236let lastCutRange: Range | null = null;237selections.sort((a, b) => Position.compare(a.getStartPosition(), b.getEndPosition()));238for (let i = 0, len = selections.length; i < len; i++) {239const selection = selections[i];240241if (selection.isEmpty()) {242if (config.emptySelectionClipboard) {243// This is a full line cut244245const position = selection.getPosition();246247let startLineNumber: number,248startColumn: number,249endLineNumber: number,250endColumn: number;251252if (position.lineNumber < model.getLineCount()) {253// Cutting a line in the middle of the model254startLineNumber = position.lineNumber;255startColumn = 1;256endLineNumber = position.lineNumber + 1;257endColumn = 1;258} else if (position.lineNumber > 1 && lastCutRange?.endLineNumber !== position.lineNumber) {259// Cutting the last line & there are more than 1 lines in the model & a previous cut operation does not touch the current cut operation260startLineNumber = position.lineNumber - 1;261startColumn = model.getLineMaxColumn(position.lineNumber - 1);262endLineNumber = position.lineNumber;263endColumn = model.getLineMaxColumn(position.lineNumber);264} else {265// Cutting the single line that the model contains266startLineNumber = position.lineNumber;267startColumn = 1;268endLineNumber = position.lineNumber;269endColumn = model.getLineMaxColumn(position.lineNumber);270}271272const deleteSelection = new Range(273startLineNumber,274startColumn,275endLineNumber,276endColumn277);278lastCutRange = deleteSelection;279280if (!deleteSelection.isEmpty()) {281commands[i] = new ReplaceCommand(deleteSelection, '');282} else {283commands[i] = null;284}285} else {286// Cannot cut empty selection287commands[i] = null;288}289} else {290commands[i] = new ReplaceCommand(selection, '');291}292}293return new EditOperationResult(EditOperationType.Other, commands, {294shouldPushStackElementBefore: true,295shouldPushStackElementAfter: true296});297}298}299300301