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