Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/cursor/cursorDeleteOperations.ts
3294 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as strings from '../../../base/common/strings.js';
7
import { ReplaceCommand } from '../commands/replaceCommand.js';
8
import { EditorAutoClosingEditStrategy, EditorAutoClosingStrategy } from '../config/editorOptions.js';
9
import { CursorConfiguration, EditOperationResult, EditOperationType, ICursorSimpleModel, isQuote } from '../cursorCommon.js';
10
import { CursorColumns } from '../core/cursorColumns.js';
11
import { MoveOperations } from './cursorMoveOperations.js';
12
import { Range } from '../core/range.js';
13
import { Selection } from '../core/selection.js';
14
import { ICommand } from '../editorCommon.js';
15
import { StandardAutoClosingPairConditional } from '../languages/languageConfiguration.js';
16
import { Position } from '../core/position.js';
17
18
export class DeleteOperations {
19
20
public static deleteRight(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[]): [boolean, Array<ICommand | null>] {
21
const commands: Array<ICommand | null> = [];
22
let shouldPushStackElementBefore = (prevEditOperationType !== EditOperationType.DeletingRight);
23
for (let i = 0, len = selections.length; i < len; i++) {
24
const selection = selections[i];
25
26
const deleteSelection = this.getDeleteRightRange(selection, model, config);
27
28
if (deleteSelection.isEmpty()) {
29
// Probably at end of file => ignore
30
commands[i] = null;
31
continue;
32
}
33
34
if (deleteSelection.startLineNumber !== deleteSelection.endLineNumber) {
35
shouldPushStackElementBefore = true;
36
}
37
38
commands[i] = new ReplaceCommand(deleteSelection, '');
39
}
40
return [shouldPushStackElementBefore, commands];
41
}
42
43
private static getDeleteRightRange(selection: Selection, model: ICursorSimpleModel, config: CursorConfiguration): Range {
44
if (!selection.isEmpty()) {
45
return selection;
46
}
47
48
const position = selection.getPosition();
49
const rightOfPosition = MoveOperations.right(config, model, position);
50
51
if (config.trimWhitespaceOnDelete && rightOfPosition.lineNumber !== position.lineNumber) {
52
// Smart line join (deleting leading whitespace) is on
53
// (and) Delete is happening at the end of a line
54
const currentLineHasContent = (model.getLineFirstNonWhitespaceColumn(position.lineNumber) > 0);
55
const firstNonWhitespaceColumn = model.getLineFirstNonWhitespaceColumn(rightOfPosition.lineNumber);
56
if (currentLineHasContent && firstNonWhitespaceColumn > 0) {
57
// The next line has content
58
return new Range(
59
rightOfPosition.lineNumber,
60
firstNonWhitespaceColumn,
61
position.lineNumber,
62
position.column
63
);
64
}
65
}
66
67
return new Range(
68
rightOfPosition.lineNumber,
69
rightOfPosition.column,
70
position.lineNumber,
71
position.column
72
);
73
}
74
75
public static isAutoClosingPairDelete(
76
autoClosingDelete: EditorAutoClosingEditStrategy,
77
autoClosingBrackets: EditorAutoClosingStrategy,
78
autoClosingQuotes: EditorAutoClosingStrategy,
79
autoClosingPairsOpen: Map<string, StandardAutoClosingPairConditional[]>,
80
model: ICursorSimpleModel,
81
selections: Selection[],
82
autoClosedCharacters: Range[]
83
): boolean {
84
if (autoClosingBrackets === 'never' && autoClosingQuotes === 'never') {
85
return false;
86
}
87
if (autoClosingDelete === 'never') {
88
return false;
89
}
90
91
for (let i = 0, len = selections.length; i < len; i++) {
92
const selection = selections[i];
93
const position = selection.getPosition();
94
95
if (!selection.isEmpty()) {
96
return false;
97
}
98
99
const lineText = model.getLineContent(position.lineNumber);
100
if (position.column < 2 || position.column >= lineText.length + 1) {
101
return false;
102
}
103
const character = lineText.charAt(position.column - 2);
104
105
const autoClosingPairCandidates = autoClosingPairsOpen.get(character);
106
if (!autoClosingPairCandidates) {
107
return false;
108
}
109
110
if (isQuote(character)) {
111
if (autoClosingQuotes === 'never') {
112
return false;
113
}
114
} else {
115
if (autoClosingBrackets === 'never') {
116
return false;
117
}
118
}
119
120
const afterCharacter = lineText.charAt(position.column - 1);
121
122
let foundAutoClosingPair = false;
123
for (const autoClosingPairCandidate of autoClosingPairCandidates) {
124
if (autoClosingPairCandidate.open === character && autoClosingPairCandidate.close === afterCharacter) {
125
foundAutoClosingPair = true;
126
}
127
}
128
if (!foundAutoClosingPair) {
129
return false;
130
}
131
132
// Must delete the pair only if it was automatically inserted by the editor
133
if (autoClosingDelete === 'auto') {
134
let found = false;
135
for (let j = 0, lenJ = autoClosedCharacters.length; j < lenJ; j++) {
136
const autoClosedCharacter = autoClosedCharacters[j];
137
if (position.lineNumber === autoClosedCharacter.startLineNumber && position.column === autoClosedCharacter.startColumn) {
138
found = true;
139
break;
140
}
141
}
142
if (!found) {
143
return false;
144
}
145
}
146
}
147
148
return true;
149
}
150
151
private static _runAutoClosingPairDelete(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[]): [boolean, ICommand[]] {
152
const commands: ICommand[] = [];
153
for (let i = 0, len = selections.length; i < len; i++) {
154
const position = selections[i].getPosition();
155
const deleteSelection = new Range(
156
position.lineNumber,
157
position.column - 1,
158
position.lineNumber,
159
position.column + 1
160
);
161
commands[i] = new ReplaceCommand(deleteSelection, '');
162
}
163
return [true, commands];
164
}
165
166
public static deleteLeft(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[], autoClosedCharacters: Range[]): [boolean, Array<ICommand | null>] {
167
if (this.isAutoClosingPairDelete(config.autoClosingDelete, config.autoClosingBrackets, config.autoClosingQuotes, config.autoClosingPairs.autoClosingPairsOpenByEnd, model, selections, autoClosedCharacters)) {
168
return this._runAutoClosingPairDelete(config, model, selections);
169
}
170
171
const commands: Array<ICommand | null> = [];
172
let shouldPushStackElementBefore = (prevEditOperationType !== EditOperationType.DeletingLeft);
173
for (let i = 0, len = selections.length; i < len; i++) {
174
const deleteRange = DeleteOperations.getDeleteLeftRange(selections[i], model, config);
175
176
// Ignore empty delete ranges, as they have no effect
177
// They happen if the cursor is at the beginning of the file.
178
if (deleteRange.isEmpty()) {
179
commands[i] = null;
180
continue;
181
}
182
183
if (deleteRange.startLineNumber !== deleteRange.endLineNumber) {
184
shouldPushStackElementBefore = true;
185
}
186
187
commands[i] = new ReplaceCommand(deleteRange, '');
188
}
189
return [shouldPushStackElementBefore, commands];
190
191
}
192
193
private static getDeleteLeftRange(selection: Selection, model: ICursorSimpleModel, config: CursorConfiguration): Range {
194
if (!selection.isEmpty()) {
195
return selection;
196
}
197
198
const position = selection.getPosition();
199
200
// Unintend when using tab stops and cursor is within indentation
201
if (config.useTabStops && position.column > 1) {
202
const lineContent = model.getLineContent(position.lineNumber);
203
204
const firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent);
205
const lastIndentationColumn = (
206
firstNonWhitespaceIndex === -1
207
? /* entire string is whitespace */ lineContent.length + 1
208
: firstNonWhitespaceIndex + 1
209
);
210
211
if (position.column <= lastIndentationColumn) {
212
const fromVisibleColumn = config.visibleColumnFromColumn(model, position);
213
const toVisibleColumn = CursorColumns.prevIndentTabStop(fromVisibleColumn, config.indentSize);
214
const toColumn = config.columnFromVisibleColumn(model, position.lineNumber, toVisibleColumn);
215
return new Range(position.lineNumber, toColumn, position.lineNumber, position.column);
216
}
217
}
218
219
return Range.fromPositions(DeleteOperations.getPositionAfterDeleteLeft(position, model), position);
220
}
221
222
private static getPositionAfterDeleteLeft(position: Position, model: ICursorSimpleModel): Position {
223
if (position.column > 1) {
224
// Convert 1-based columns to 0-based offsets and back.
225
const idx = strings.getLeftDeleteOffset(position.column - 1, model.getLineContent(position.lineNumber));
226
return position.with(undefined, idx + 1);
227
} else if (position.lineNumber > 1) {
228
const newLine = position.lineNumber - 1;
229
return new Position(newLine, model.getLineMaxColumn(newLine));
230
} else {
231
return position;
232
}
233
}
234
235
public static cut(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[]): EditOperationResult {
236
const commands: Array<ICommand | null> = [];
237
let lastCutRange: Range | null = null;
238
selections.sort((a, b) => Position.compare(a.getStartPosition(), b.getEndPosition()));
239
for (let i = 0, len = selections.length; i < len; i++) {
240
const selection = selections[i];
241
242
if (selection.isEmpty()) {
243
if (config.emptySelectionClipboard) {
244
// This is a full line cut
245
246
const position = selection.getPosition();
247
248
let startLineNumber: number,
249
startColumn: number,
250
endLineNumber: number,
251
endColumn: number;
252
253
if (position.lineNumber < model.getLineCount()) {
254
// Cutting a line in the middle of the model
255
startLineNumber = position.lineNumber;
256
startColumn = 1;
257
endLineNumber = position.lineNumber + 1;
258
endColumn = 1;
259
} else if (position.lineNumber > 1 && lastCutRange?.endLineNumber !== position.lineNumber) {
260
// Cutting the last line & there are more than 1 lines in the model & a previous cut operation does not touch the current cut operation
261
startLineNumber = position.lineNumber - 1;
262
startColumn = model.getLineMaxColumn(position.lineNumber - 1);
263
endLineNumber = position.lineNumber;
264
endColumn = model.getLineMaxColumn(position.lineNumber);
265
} else {
266
// Cutting the single line that the model contains
267
startLineNumber = position.lineNumber;
268
startColumn = 1;
269
endLineNumber = position.lineNumber;
270
endColumn = model.getLineMaxColumn(position.lineNumber);
271
}
272
273
const deleteSelection = new Range(
274
startLineNumber,
275
startColumn,
276
endLineNumber,
277
endColumn
278
);
279
lastCutRange = deleteSelection;
280
281
if (!deleteSelection.isEmpty()) {
282
commands[i] = new ReplaceCommand(deleteSelection, '');
283
} else {
284
commands[i] = null;
285
}
286
} else {
287
// Cannot cut empty selection
288
commands[i] = null;
289
}
290
} else {
291
commands[i] = new ReplaceCommand(selection, '');
292
}
293
}
294
return new EditOperationResult(EditOperationType.Other, commands, {
295
shouldPushStackElementBefore: true,
296
shouldPushStackElementAfter: true
297
});
298
}
299
}
300
301