Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as strings from '../../../../base/common/strings.js';
7
import { ShiftCommand } from '../../../common/commands/shiftCommand.js';
8
import { EditorAutoIndentStrategy } from '../../../common/config/editorOptions.js';
9
import { Range } from '../../../common/core/range.js';
10
import { Selection } from '../../../common/core/selection.js';
11
import { ICommand, ICursorStateComputerData, IEditOperationBuilder } from '../../../common/editorCommon.js';
12
import { ITextModel } from '../../../common/model.js';
13
import { CompleteEnterAction, IndentAction } from '../../../common/languages/languageConfiguration.js';
14
import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js';
15
import { IndentConsts } from '../../../common/languages/supports/indentRules.js';
16
import * as indentUtils from '../../indentation/common/indentUtils.js';
17
import { getGoodIndentForLine, getIndentMetadata, IIndentConverter, IVirtualModel } from '../../../common/languages/autoIndent.js';
18
import { getEnterAction } from '../../../common/languages/enterAction.js';
19
20
export class MoveLinesCommand implements ICommand {
21
22
private readonly _selection: Selection;
23
private readonly _isMovingDown: boolean;
24
private readonly _autoIndent: EditorAutoIndentStrategy;
25
26
private _selectionId: string | null;
27
private _moveEndPositionDown?: boolean;
28
private _moveEndLineSelectionShrink: boolean;
29
30
constructor(
31
selection: Selection,
32
isMovingDown: boolean,
33
autoIndent: EditorAutoIndentStrategy,
34
@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService
35
) {
36
this._selection = selection;
37
this._isMovingDown = isMovingDown;
38
this._autoIndent = autoIndent;
39
this._selectionId = null;
40
this._moveEndLineSelectionShrink = false;
41
}
42
43
public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void {
44
45
const getLanguageId = () => {
46
return model.getLanguageId();
47
};
48
const getLanguageIdAtPosition = (lineNumber: number, column: number) => {
49
return model.getLanguageIdAtPosition(lineNumber, column);
50
};
51
52
const modelLineCount = model.getLineCount();
53
54
if (this._isMovingDown && this._selection.endLineNumber === modelLineCount) {
55
this._selectionId = builder.trackSelection(this._selection);
56
return;
57
}
58
if (!this._isMovingDown && this._selection.startLineNumber === 1) {
59
this._selectionId = builder.trackSelection(this._selection);
60
return;
61
}
62
63
this._moveEndPositionDown = false;
64
let s = this._selection;
65
66
if (s.startLineNumber < s.endLineNumber && s.endColumn === 1) {
67
this._moveEndPositionDown = true;
68
s = s.setEndPosition(s.endLineNumber - 1, model.getLineMaxColumn(s.endLineNumber - 1));
69
}
70
71
const { tabSize, indentSize, insertSpaces } = model.getOptions();
72
const indentConverter = this.buildIndentConverter(tabSize, indentSize, insertSpaces);
73
74
if (s.startLineNumber === s.endLineNumber && model.getLineMaxColumn(s.startLineNumber) === 1) {
75
// Current line is empty
76
const lineNumber = s.startLineNumber;
77
const otherLineNumber = (this._isMovingDown ? lineNumber + 1 : lineNumber - 1);
78
79
if (model.getLineMaxColumn(otherLineNumber) === 1) {
80
// Other line number is empty too, so no editing is needed
81
// Add a no-op to force running by the model
82
builder.addEditOperation(new Range(1, 1, 1, 1), null);
83
} else {
84
// Type content from other line number on line number
85
builder.addEditOperation(new Range(lineNumber, 1, lineNumber, 1), model.getLineContent(otherLineNumber));
86
87
// Remove content from other line number
88
builder.addEditOperation(new Range(otherLineNumber, 1, otherLineNumber, model.getLineMaxColumn(otherLineNumber)), null);
89
}
90
// Track selection at the other line number
91
s = new Selection(otherLineNumber, 1, otherLineNumber, 1);
92
93
} else {
94
95
let movingLineNumber: number;
96
let movingLineText: string;
97
98
if (this._isMovingDown) {
99
movingLineNumber = s.endLineNumber + 1;
100
movingLineText = model.getLineContent(movingLineNumber);
101
// Delete line that needs to be moved
102
builder.addEditOperation(new Range(movingLineNumber - 1, model.getLineMaxColumn(movingLineNumber - 1), movingLineNumber, model.getLineMaxColumn(movingLineNumber)), null);
103
104
let insertingText = movingLineText;
105
106
if (this.shouldAutoIndent(model, s)) {
107
const movingLineMatchResult = this.matchEnterRule(model, indentConverter, tabSize, movingLineNumber, s.startLineNumber - 1);
108
// if s.startLineNumber - 1 matches onEnter rule, we still honor that.
109
if (movingLineMatchResult !== null) {
110
const oldIndentation = strings.getLeadingWhitespace(model.getLineContent(movingLineNumber));
111
const newSpaceCnt = movingLineMatchResult + indentUtils.getSpaceCnt(oldIndentation, tabSize);
112
const newIndentation = indentUtils.generateIndent(newSpaceCnt, tabSize, insertSpaces);
113
insertingText = newIndentation + this.trimStart(movingLineText);
114
} else {
115
// no enter rule matches, let's check indentatin rules then.
116
const virtualModel: IVirtualModel = {
117
tokenization: {
118
getLineTokens: (lineNumber: number) => {
119
if (lineNumber === s.startLineNumber) {
120
return model.tokenization.getLineTokens(movingLineNumber);
121
} else {
122
return model.tokenization.getLineTokens(lineNumber);
123
}
124
},
125
getLanguageId,
126
getLanguageIdAtPosition,
127
},
128
getLineContent: (lineNumber: number) => {
129
if (lineNumber === s.startLineNumber) {
130
return model.getLineContent(movingLineNumber);
131
} else {
132
return model.getLineContent(lineNumber);
133
}
134
},
135
};
136
const indentOfMovingLine = getGoodIndentForLine(
137
this._autoIndent,
138
virtualModel,
139
model.getLanguageIdAtPosition(movingLineNumber, 1),
140
s.startLineNumber,
141
indentConverter,
142
this._languageConfigurationService
143
);
144
if (indentOfMovingLine !== null) {
145
const oldIndentation = strings.getLeadingWhitespace(model.getLineContent(movingLineNumber));
146
const newSpaceCnt = indentUtils.getSpaceCnt(indentOfMovingLine, tabSize);
147
const oldSpaceCnt = indentUtils.getSpaceCnt(oldIndentation, tabSize);
148
if (newSpaceCnt !== oldSpaceCnt) {
149
const newIndentation = indentUtils.generateIndent(newSpaceCnt, tabSize, insertSpaces);
150
insertingText = newIndentation + this.trimStart(movingLineText);
151
}
152
}
153
}
154
155
// add edit operations for moving line first to make sure it's executed after we make indentation change
156
// to s.startLineNumber
157
builder.addEditOperation(new Range(s.startLineNumber, 1, s.startLineNumber, 1), insertingText + '\n');
158
159
const ret = this.matchEnterRuleMovingDown(model, indentConverter, tabSize, s.startLineNumber, movingLineNumber, insertingText);
160
161
// check if the line being moved before matches onEnter rules, if so let's adjust the indentation by onEnter rules.
162
if (ret !== null) {
163
if (ret !== 0) {
164
this.getIndentEditsOfMovingBlock(model, builder, s, tabSize, insertSpaces, ret);
165
}
166
} else {
167
// it doesn't match onEnter rules, let's check indentation rules then.
168
const virtualModel: IVirtualModel = {
169
tokenization: {
170
getLineTokens: (lineNumber: number) => {
171
if (lineNumber === s.startLineNumber) {
172
// TODO@aiday-mar: the tokens here don't correspond exactly to the corresponding content (after indentation adjustment), have to fix this.
173
return model.tokenization.getLineTokens(movingLineNumber);
174
} else if (lineNumber >= s.startLineNumber + 1 && lineNumber <= s.endLineNumber + 1) {
175
return model.tokenization.getLineTokens(lineNumber - 1);
176
} else {
177
return model.tokenization.getLineTokens(lineNumber);
178
}
179
},
180
getLanguageId,
181
getLanguageIdAtPosition,
182
},
183
getLineContent: (lineNumber: number) => {
184
if (lineNumber === s.startLineNumber) {
185
return insertingText;
186
} else if (lineNumber >= s.startLineNumber + 1 && lineNumber <= s.endLineNumber + 1) {
187
return model.getLineContent(lineNumber - 1);
188
} else {
189
return model.getLineContent(lineNumber);
190
}
191
},
192
};
193
194
const newIndentatOfMovingBlock = getGoodIndentForLine(
195
this._autoIndent,
196
virtualModel,
197
model.getLanguageIdAtPosition(movingLineNumber, 1),
198
s.startLineNumber + 1,
199
indentConverter,
200
this._languageConfigurationService
201
);
202
203
if (newIndentatOfMovingBlock !== null) {
204
const oldIndentation = strings.getLeadingWhitespace(model.getLineContent(s.startLineNumber));
205
const newSpaceCnt = indentUtils.getSpaceCnt(newIndentatOfMovingBlock, tabSize);
206
const oldSpaceCnt = indentUtils.getSpaceCnt(oldIndentation, tabSize);
207
if (newSpaceCnt !== oldSpaceCnt) {
208
const spaceCntOffset = newSpaceCnt - oldSpaceCnt;
209
210
this.getIndentEditsOfMovingBlock(model, builder, s, tabSize, insertSpaces, spaceCntOffset);
211
}
212
}
213
}
214
} else {
215
// Insert line that needs to be moved before
216
builder.addEditOperation(new Range(s.startLineNumber, 1, s.startLineNumber, 1), insertingText + '\n');
217
}
218
} else {
219
movingLineNumber = s.startLineNumber - 1;
220
movingLineText = model.getLineContent(movingLineNumber);
221
222
// Delete line that needs to be moved
223
builder.addEditOperation(new Range(movingLineNumber, 1, movingLineNumber + 1, 1), null);
224
225
// Insert line that needs to be moved after
226
builder.addEditOperation(new Range(s.endLineNumber, model.getLineMaxColumn(s.endLineNumber), s.endLineNumber, model.getLineMaxColumn(s.endLineNumber)), '\n' + movingLineText);
227
228
if (this.shouldAutoIndent(model, s)) {
229
const virtualModel: IVirtualModel = {
230
tokenization: {
231
getLineTokens: (lineNumber: number) => {
232
if (lineNumber === movingLineNumber) {
233
return model.tokenization.getLineTokens(s.startLineNumber);
234
} else {
235
return model.tokenization.getLineTokens(lineNumber);
236
}
237
},
238
getLanguageId,
239
getLanguageIdAtPosition,
240
},
241
getLineContent: (lineNumber: number) => {
242
if (lineNumber === movingLineNumber) {
243
return model.getLineContent(s.startLineNumber);
244
} else {
245
return model.getLineContent(lineNumber);
246
}
247
},
248
};
249
250
const ret = this.matchEnterRule(model, indentConverter, tabSize, s.startLineNumber, s.startLineNumber - 2);
251
// check if s.startLineNumber - 2 matches onEnter rules, if so adjust the moving block by onEnter rules.
252
if (ret !== null) {
253
if (ret !== 0) {
254
this.getIndentEditsOfMovingBlock(model, builder, s, tabSize, insertSpaces, ret);
255
}
256
} else {
257
// it doesn't match any onEnter rule, let's check indentation rules then.
258
const indentOfFirstLine = getGoodIndentForLine(
259
this._autoIndent,
260
virtualModel,
261
model.getLanguageIdAtPosition(s.startLineNumber, 1),
262
movingLineNumber,
263
indentConverter,
264
this._languageConfigurationService
265
);
266
if (indentOfFirstLine !== null) {
267
// adjust the indentation of the moving block
268
const oldIndent = strings.getLeadingWhitespace(model.getLineContent(s.startLineNumber));
269
const newSpaceCnt = indentUtils.getSpaceCnt(indentOfFirstLine, tabSize);
270
const oldSpaceCnt = indentUtils.getSpaceCnt(oldIndent, tabSize);
271
if (newSpaceCnt !== oldSpaceCnt) {
272
const spaceCntOffset = newSpaceCnt - oldSpaceCnt;
273
274
this.getIndentEditsOfMovingBlock(model, builder, s, tabSize, insertSpaces, spaceCntOffset);
275
}
276
}
277
}
278
}
279
}
280
}
281
282
this._selectionId = builder.trackSelection(s);
283
}
284
285
private buildIndentConverter(tabSize: number, indentSize: number, insertSpaces: boolean): IIndentConverter {
286
return {
287
shiftIndent: (indentation) => {
288
return ShiftCommand.shiftIndent(indentation, indentation.length + 1, tabSize, indentSize, insertSpaces);
289
},
290
unshiftIndent: (indentation) => {
291
return ShiftCommand.unshiftIndent(indentation, indentation.length + 1, tabSize, indentSize, insertSpaces);
292
}
293
};
294
}
295
296
private parseEnterResult(model: ITextModel, indentConverter: IIndentConverter, tabSize: number, line: number, enter: CompleteEnterAction | null) {
297
if (enter) {
298
let enterPrefix = enter.indentation;
299
300
if (enter.indentAction === IndentAction.None) {
301
enterPrefix = enter.indentation + enter.appendText;
302
} else if (enter.indentAction === IndentAction.Indent) {
303
enterPrefix = enter.indentation + enter.appendText;
304
} else if (enter.indentAction === IndentAction.IndentOutdent) {
305
enterPrefix = enter.indentation;
306
} else if (enter.indentAction === IndentAction.Outdent) {
307
enterPrefix = indentConverter.unshiftIndent(enter.indentation) + enter.appendText;
308
}
309
const movingLineText = model.getLineContent(line);
310
if (this.trimStart(movingLineText).indexOf(this.trimStart(enterPrefix)) >= 0) {
311
const oldIndentation = strings.getLeadingWhitespace(model.getLineContent(line));
312
let newIndentation = strings.getLeadingWhitespace(enterPrefix);
313
const indentMetadataOfMovelingLine = getIndentMetadata(model, line, this._languageConfigurationService);
314
if (indentMetadataOfMovelingLine !== null && indentMetadataOfMovelingLine & IndentConsts.DECREASE_MASK) {
315
newIndentation = indentConverter.unshiftIndent(newIndentation);
316
}
317
const newSpaceCnt = indentUtils.getSpaceCnt(newIndentation, tabSize);
318
const oldSpaceCnt = indentUtils.getSpaceCnt(oldIndentation, tabSize);
319
return newSpaceCnt - oldSpaceCnt;
320
}
321
}
322
323
return null;
324
}
325
326
/**
327
*
328
* @param model
329
* @param indentConverter
330
* @param tabSize
331
* @param line the line moving down
332
* @param futureAboveLineNumber the line which will be at the `line` position
333
* @param futureAboveLineText
334
*/
335
private matchEnterRuleMovingDown(model: ITextModel, indentConverter: IIndentConverter, tabSize: number, line: number, futureAboveLineNumber: number, futureAboveLineText: string) {
336
if (strings.lastNonWhitespaceIndex(futureAboveLineText) >= 0) {
337
// break
338
const maxColumn = model.getLineMaxColumn(futureAboveLineNumber);
339
const enter = getEnterAction(this._autoIndent, model, new Range(futureAboveLineNumber, maxColumn, futureAboveLineNumber, maxColumn), this._languageConfigurationService);
340
return this.parseEnterResult(model, indentConverter, tabSize, line, enter);
341
} else {
342
// go upwards, starting from `line - 1`
343
let validPrecedingLine = line - 1;
344
while (validPrecedingLine >= 1) {
345
const lineContent = model.getLineContent(validPrecedingLine);
346
const nonWhitespaceIdx = strings.lastNonWhitespaceIndex(lineContent);
347
348
if (nonWhitespaceIdx >= 0) {
349
break;
350
}
351
352
validPrecedingLine--;
353
}
354
355
if (validPrecedingLine < 1 || line > model.getLineCount()) {
356
return null;
357
}
358
359
const maxColumn = model.getLineMaxColumn(validPrecedingLine);
360
const enter = getEnterAction(this._autoIndent, model, new Range(validPrecedingLine, maxColumn, validPrecedingLine, maxColumn), this._languageConfigurationService);
361
return this.parseEnterResult(model, indentConverter, tabSize, line, enter);
362
}
363
}
364
365
private matchEnterRule(model: ITextModel, indentConverter: IIndentConverter, tabSize: number, line: number, oneLineAbove: number, previousLineText?: string) {
366
let validPrecedingLine = oneLineAbove;
367
while (validPrecedingLine >= 1) {
368
// ship empty lines as empty lines just inherit indentation
369
let lineContent;
370
if (validPrecedingLine === oneLineAbove && previousLineText !== undefined) {
371
lineContent = previousLineText;
372
} else {
373
lineContent = model.getLineContent(validPrecedingLine);
374
}
375
376
const nonWhitespaceIdx = strings.lastNonWhitespaceIndex(lineContent);
377
if (nonWhitespaceIdx >= 0) {
378
break;
379
}
380
validPrecedingLine--;
381
}
382
383
if (validPrecedingLine < 1 || line > model.getLineCount()) {
384
return null;
385
}
386
387
const maxColumn = model.getLineMaxColumn(validPrecedingLine);
388
const enter = getEnterAction(this._autoIndent, model, new Range(validPrecedingLine, maxColumn, validPrecedingLine, maxColumn), this._languageConfigurationService);
389
return this.parseEnterResult(model, indentConverter, tabSize, line, enter);
390
}
391
392
private trimStart(str: string) {
393
return str.replace(/^\s+/, '');
394
}
395
396
private shouldAutoIndent(model: ITextModel, selection: Selection) {
397
if (this._autoIndent < EditorAutoIndentStrategy.Full) {
398
return false;
399
}
400
// if it's not easy to tokenize, we stop auto indent.
401
if (!model.tokenization.isCheapToTokenize(selection.startLineNumber)) {
402
return false;
403
}
404
const languageAtSelectionStart = model.getLanguageIdAtPosition(selection.startLineNumber, 1);
405
const languageAtSelectionEnd = model.getLanguageIdAtPosition(selection.endLineNumber, 1);
406
407
if (languageAtSelectionStart !== languageAtSelectionEnd) {
408
return false;
409
}
410
411
if (this._languageConfigurationService.getLanguageConfiguration(languageAtSelectionStart).indentRulesSupport === null) {
412
return false;
413
}
414
415
return true;
416
}
417
418
private getIndentEditsOfMovingBlock(model: ITextModel, builder: IEditOperationBuilder, s: Selection, tabSize: number, insertSpaces: boolean, offset: number) {
419
for (let i = s.startLineNumber; i <= s.endLineNumber; i++) {
420
const lineContent = model.getLineContent(i);
421
const originalIndent = strings.getLeadingWhitespace(lineContent);
422
const originalSpacesCnt = indentUtils.getSpaceCnt(originalIndent, tabSize);
423
const newSpacesCnt = originalSpacesCnt + offset;
424
const newIndent = indentUtils.generateIndent(newSpacesCnt, tabSize, insertSpaces);
425
426
if (newIndent !== originalIndent) {
427
builder.addEditOperation(new Range(i, 1, i, originalIndent.length + 1), newIndent);
428
429
if (i === s.endLineNumber && s.endColumn <= originalIndent.length + 1 && newIndent === '') {
430
// as users select part of the original indent white spaces
431
// when we adjust the indentation of endLine, we should adjust the cursor position as well.
432
this._moveEndLineSelectionShrink = true;
433
}
434
}
435
436
}
437
}
438
439
public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection {
440
let result = helper.getTrackedSelection(this._selectionId!);
441
442
if (this._moveEndPositionDown) {
443
result = result.setEndPosition(result.endLineNumber + 1, 1);
444
}
445
446
if (this._moveEndLineSelectionShrink && result.startLineNumber < result.endLineNumber) {
447
result = result.setEndPosition(result.endLineNumber, 2);
448
}
449
450
return result;
451
}
452
}
453
454