Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/commands/shiftCommand.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 { CharCode } from '../../../base/common/charCode.js';
7
import * as strings from '../../../base/common/strings.js';
8
import { CursorColumns } from '../core/cursorColumns.js';
9
import { Range } from '../core/range.js';
10
import { Selection, SelectionDirection } from '../core/selection.js';
11
import { ICommand, ICursorStateComputerData, IEditOperationBuilder } from '../editorCommon.js';
12
import { ITextModel } from '../model.js';
13
import { EditorAutoIndentStrategy } from '../config/editorOptions.js';
14
import { getEnterAction } from '../languages/enterAction.js';
15
import { ILanguageConfigurationService } from '../languages/languageConfigurationRegistry.js';
16
17
export interface IShiftCommandOpts {
18
isUnshift: boolean;
19
tabSize: number;
20
indentSize: number;
21
insertSpaces: boolean;
22
useTabStops: boolean;
23
autoIndent: EditorAutoIndentStrategy;
24
}
25
26
const repeatCache: { [str: string]: string[] } = Object.create(null);
27
function cachedStringRepeat(str: string, count: number): string {
28
if (count <= 0) {
29
return '';
30
}
31
if (!repeatCache[str]) {
32
repeatCache[str] = ['', str];
33
}
34
const cache = repeatCache[str];
35
for (let i = cache.length; i <= count; i++) {
36
cache[i] = cache[i - 1] + str;
37
}
38
return cache[count];
39
}
40
41
export class ShiftCommand implements ICommand {
42
43
public static unshiftIndent(line: string, column: number, tabSize: number, indentSize: number, insertSpaces: boolean): string {
44
// Determine the visible column where the content starts
45
const contentStartVisibleColumn = CursorColumns.visibleColumnFromColumn(line, column, tabSize);
46
47
if (insertSpaces) {
48
const indent = cachedStringRepeat(' ', indentSize);
49
const desiredTabStop = CursorColumns.prevIndentTabStop(contentStartVisibleColumn, indentSize);
50
const indentCount = desiredTabStop / indentSize; // will be an integer
51
return cachedStringRepeat(indent, indentCount);
52
} else {
53
const indent = '\t';
54
const desiredTabStop = CursorColumns.prevRenderTabStop(contentStartVisibleColumn, tabSize);
55
const indentCount = desiredTabStop / tabSize; // will be an integer
56
return cachedStringRepeat(indent, indentCount);
57
}
58
}
59
60
public static shiftIndent(line: string, column: number, tabSize: number, indentSize: number, insertSpaces: boolean): string {
61
// Determine the visible column where the content starts
62
const contentStartVisibleColumn = CursorColumns.visibleColumnFromColumn(line, column, tabSize);
63
64
if (insertSpaces) {
65
const indent = cachedStringRepeat(' ', indentSize);
66
const desiredTabStop = CursorColumns.nextIndentTabStop(contentStartVisibleColumn, indentSize);
67
const indentCount = desiredTabStop / indentSize; // will be an integer
68
return cachedStringRepeat(indent, indentCount);
69
} else {
70
const indent = '\t';
71
const desiredTabStop = CursorColumns.nextRenderTabStop(contentStartVisibleColumn, tabSize);
72
const indentCount = desiredTabStop / tabSize; // will be an integer
73
return cachedStringRepeat(indent, indentCount);
74
}
75
}
76
77
private readonly _opts: IShiftCommandOpts;
78
private readonly _selection: Selection;
79
private _selectionId: string | null;
80
private _useLastEditRangeForCursorEndPosition: boolean;
81
private _selectionStartColumnStaysPut: boolean;
82
83
constructor(
84
range: Selection,
85
opts: IShiftCommandOpts,
86
@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService
87
) {
88
this._opts = opts;
89
this._selection = range;
90
this._selectionId = null;
91
this._useLastEditRangeForCursorEndPosition = false;
92
this._selectionStartColumnStaysPut = false;
93
}
94
95
private _addEditOperation(builder: IEditOperationBuilder, range: Range, text: string) {
96
if (this._useLastEditRangeForCursorEndPosition) {
97
builder.addTrackedEditOperation(range, text);
98
} else {
99
builder.addEditOperation(range, text);
100
}
101
}
102
103
public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void {
104
const startLine = this._selection.startLineNumber;
105
106
let endLine = this._selection.endLineNumber;
107
if (this._selection.endColumn === 1 && startLine !== endLine) {
108
endLine = endLine - 1;
109
}
110
111
const { tabSize, indentSize, insertSpaces } = this._opts;
112
const shouldIndentEmptyLines = (startLine === endLine);
113
114
if (this._opts.useTabStops) {
115
// if indenting or outdenting on a whitespace only line
116
if (this._selection.isEmpty()) {
117
if (/^\s*$/.test(model.getLineContent(startLine))) {
118
this._useLastEditRangeForCursorEndPosition = true;
119
}
120
}
121
122
// keep track of previous line's "miss-alignment"
123
let previousLineExtraSpaces = 0, extraSpaces = 0;
124
for (let lineNumber = startLine; lineNumber <= endLine; lineNumber++, previousLineExtraSpaces = extraSpaces) {
125
extraSpaces = 0;
126
const lineText = model.getLineContent(lineNumber);
127
let indentationEndIndex = strings.firstNonWhitespaceIndex(lineText);
128
129
if (this._opts.isUnshift && (lineText.length === 0 || indentationEndIndex === 0)) {
130
// empty line or line with no leading whitespace => nothing to do
131
continue;
132
}
133
134
if (!shouldIndentEmptyLines && !this._opts.isUnshift && lineText.length === 0) {
135
// do not indent empty lines => nothing to do
136
continue;
137
}
138
139
if (indentationEndIndex === -1) {
140
// the entire line is whitespace
141
indentationEndIndex = lineText.length;
142
}
143
144
if (lineNumber > 1) {
145
const contentStartVisibleColumn = CursorColumns.visibleColumnFromColumn(lineText, indentationEndIndex + 1, tabSize);
146
if (contentStartVisibleColumn % indentSize !== 0) {
147
// The current line is "miss-aligned", so let's see if this is expected...
148
// This can only happen when it has trailing commas in the indent
149
if (model.tokenization.isCheapToTokenize(lineNumber - 1)) {
150
const enterAction = getEnterAction(this._opts.autoIndent, model, new Range(lineNumber - 1, model.getLineMaxColumn(lineNumber - 1), lineNumber - 1, model.getLineMaxColumn(lineNumber - 1)), this._languageConfigurationService);
151
if (enterAction) {
152
extraSpaces = previousLineExtraSpaces;
153
if (enterAction.appendText) {
154
for (let j = 0, lenJ = enterAction.appendText.length; j < lenJ && extraSpaces < indentSize; j++) {
155
if (enterAction.appendText.charCodeAt(j) === CharCode.Space) {
156
extraSpaces++;
157
} else {
158
break;
159
}
160
}
161
}
162
if (enterAction.removeText) {
163
extraSpaces = Math.max(0, extraSpaces - enterAction.removeText);
164
}
165
166
// Act as if `prefixSpaces` is not part of the indentation
167
for (let j = 0; j < extraSpaces; j++) {
168
if (indentationEndIndex === 0 || lineText.charCodeAt(indentationEndIndex - 1) !== CharCode.Space) {
169
break;
170
}
171
indentationEndIndex--;
172
}
173
}
174
}
175
}
176
}
177
178
179
if (this._opts.isUnshift && indentationEndIndex === 0) {
180
// line with no leading whitespace => nothing to do
181
continue;
182
}
183
184
let desiredIndent: string;
185
if (this._opts.isUnshift) {
186
desiredIndent = ShiftCommand.unshiftIndent(lineText, indentationEndIndex + 1, tabSize, indentSize, insertSpaces);
187
} else {
188
desiredIndent = ShiftCommand.shiftIndent(lineText, indentationEndIndex + 1, tabSize, indentSize, insertSpaces);
189
}
190
191
this._addEditOperation(builder, new Range(lineNumber, 1, lineNumber, indentationEndIndex + 1), desiredIndent);
192
if (lineNumber === startLine && !this._selection.isEmpty()) {
193
// Force the startColumn to stay put because we're inserting after it
194
this._selectionStartColumnStaysPut = (this._selection.startColumn <= indentationEndIndex + 1);
195
}
196
}
197
} else {
198
199
// if indenting or outdenting on a whitespace only line
200
if (!this._opts.isUnshift && this._selection.isEmpty() && model.getLineLength(startLine) === 0) {
201
this._useLastEditRangeForCursorEndPosition = true;
202
}
203
204
const oneIndent = (insertSpaces ? cachedStringRepeat(' ', indentSize) : '\t');
205
206
for (let lineNumber = startLine; lineNumber <= endLine; lineNumber++) {
207
const lineText = model.getLineContent(lineNumber);
208
let indentationEndIndex = strings.firstNonWhitespaceIndex(lineText);
209
210
if (this._opts.isUnshift && (lineText.length === 0 || indentationEndIndex === 0)) {
211
// empty line or line with no leading whitespace => nothing to do
212
continue;
213
}
214
215
if (!shouldIndentEmptyLines && !this._opts.isUnshift && lineText.length === 0) {
216
// do not indent empty lines => nothing to do
217
continue;
218
}
219
220
if (indentationEndIndex === -1) {
221
// the entire line is whitespace
222
indentationEndIndex = lineText.length;
223
}
224
225
if (this._opts.isUnshift && indentationEndIndex === 0) {
226
// line with no leading whitespace => nothing to do
227
continue;
228
}
229
230
if (this._opts.isUnshift) {
231
232
indentationEndIndex = Math.min(indentationEndIndex, indentSize);
233
for (let i = 0; i < indentationEndIndex; i++) {
234
const chr = lineText.charCodeAt(i);
235
if (chr === CharCode.Tab) {
236
indentationEndIndex = i + 1;
237
break;
238
}
239
}
240
241
this._addEditOperation(builder, new Range(lineNumber, 1, lineNumber, indentationEndIndex + 1), '');
242
} else {
243
this._addEditOperation(builder, new Range(lineNumber, 1, lineNumber, 1), oneIndent);
244
if (lineNumber === startLine && !this._selection.isEmpty()) {
245
// Force the startColumn to stay put because we're inserting after it
246
this._selectionStartColumnStaysPut = (this._selection.startColumn === 1);
247
}
248
}
249
}
250
}
251
252
this._selectionId = builder.trackSelection(this._selection);
253
}
254
255
public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection {
256
if (this._useLastEditRangeForCursorEndPosition) {
257
const lastOp = helper.getInverseEditOperations()[0];
258
return new Selection(lastOp.range.endLineNumber, lastOp.range.endColumn, lastOp.range.endLineNumber, lastOp.range.endColumn);
259
}
260
const result = helper.getTrackedSelection(this._selectionId!);
261
262
if (this._selectionStartColumnStaysPut) {
263
// The selection start should not move
264
const initialStartColumn = this._selection.startColumn;
265
const resultStartColumn = result.startColumn;
266
if (resultStartColumn <= initialStartColumn) {
267
return result;
268
}
269
270
if (result.getDirection() === SelectionDirection.LTR) {
271
return new Selection(result.startLineNumber, initialStartColumn, result.endLineNumber, result.endColumn);
272
}
273
return new Selection(result.endLineNumber, result.endColumn, result.startLineNumber, initialStartColumn);
274
}
275
276
return result;
277
}
278
}
279
280